const startTimeSettings = {
pomodoro: 25 * 60 * 1000,
shortBreak: 5 * 60 * 1000,
longBreak: 15 * 60 * 1000
};
let currentMode = 'pomodoro';
let remainingTimeMs = startTimeSettings[currentMode];
let countdownTimer = null;
let isRunning = false;
let targetDate = null;
const startBtn = document.getElementById('startBtn');
const modeBtns = document.querySelectorAll('.mode-btn');
function getTimeSegmentElements(segmentElement) {
const segmentDisplay = segmentElement.querySelector('.segment-display');
const segmentDisplayTop = segmentDisplay.querySelector('.segment-display__top');
const segmentDisplayBottom = segmentDisplay.querySelector('.segment-display__bottom');
const segmentOverlay = segmentDisplay.querySelector('.segment-overlay');
const segmentOverlayTop = segmentOverlay.querySelector('.segment-overlay__top');
const segmentOverlayBottom = segmentOverlay.querySelector('.segment-overlay__bottom');
return { segmentDisplayTop, segmentDisplayBottom, segmentOverlay, segmentOverlayTop, segmentOverlayBottom };
}
function updateSegmentValues(displayElement, overlayElement, value) {
displayElement.textContent = value;
overlayElement.textContent = value;
}
function updateTimeSegment(segmentElement, timeValue) {
const segmentElements = getTimeSegmentElements(segmentElement);
if (parseInt(segmentElements.segmentDisplayTop.textContent, 10) === timeValue) {
return;
}
segmentElements.segmentOverlay.classList.add('flip');
updateSegmentValues(
segmentElements.segmentDisplayTop,
segmentElements.segmentOverlayBottom,
timeValue
);
function finishAnimation() {
segmentElements.segmentOverlay.classList.remove('flip');
updateSegmentValues(
segmentElements.segmentDisplayBottom,
segmentElements.segmentOverlayTop,
timeValue
);
this.removeEventListener('animationend', finishAnimation);
}
segmentElements.segmentOverlay.addEventListener('animationend', finishAnimation);
}
function updateTimeSection(sectionID, timeValue) {
const sectionElement = document.getElementById(sectionID);
if (!sectionElement) return;
const timeGroup = sectionElement.querySelector('.time-group');
if (!timeGroup) return;
let timeStr = timeValue.toString();
if (timeStr.length < 2) timeStr = timeStr.padStart(2, '0');
let timeSegments = timeGroup.querySelectorAll('.time-segment');
// Add more segments if needed
while (timeSegments.length < timeStr.length) {
const newSeg = timeSegments[0].cloneNode(true);
const overlay = newSeg.querySelector('.segment-overlay');
if (overlay) overlay.classList.remove('flip');
const displays = newSeg.querySelectorAll('.segment-display__top, .segment-display__bottom, .segment-overlay__top, .segment-overlay__bottom');
displays.forEach(d => d.textContent = '0');
timeGroup.appendChild(newSeg);
timeSegments = timeGroup.querySelectorAll('.time-segment');
}
// Remove extra segments if not needed (but keep at least 2)
while (timeSegments.length > Math.max(2, timeStr.length)) {
timeGroup.removeChild(timeSegments[timeSegments.length - 1]);
timeSegments = timeGroup.querySelectorAll('.time-segment');
}
for (let i = 0; i < timeStr.length; i++) {
updateTimeSegment(timeSegments[i], parseInt(timeStr[i], 10));
}
}
function getTimeRemaining(targetDateTime) {
const nowTime = Date.now();
const complete = nowTime >= targetDateTime;
if (complete) {
return { complete, seconds: 0, minutes: 0, hours: 0 };
}
const secondsRemaining = Math.max(0, Math.round((targetDateTime - nowTime) / 1000));
const minutes = Math.floor(secondsRemaining / 60);
const seconds = secondsRemaining % 60;
return { complete, seconds, minutes, hours: 0 };
}
function calculateTimeBitsStatic(ms) {
if (ms <= 0) return { complete: true, seconds: 0, minutes: 0, hours: 0 };
const secondsRemaining = Math.round(ms / 1000);
const minutes = Math.floor(secondsRemaining / 60);
const seconds = secondsRemaining % 60;
return { complete: false, seconds, minutes, hours: 0 };
}
function updateAllSegments() {
let timeRemainingBits;
if (!isRunning) {
timeRemainingBits = calculateTimeBitsStatic(remainingTimeMs);
} else {
timeRemainingBits = getTimeRemaining(targetDate.getTime());
}
updateTimeSection('seconds', timeRemainingBits.seconds);
updateTimeSection('minutes', timeRemainingBits.minutes);
updateTimeSection('hours', timeRemainingBits.hours);
// Basic & Animation Time labels
let timeStr = `${timeRemainingBits.minutes.toString().padStart(2, '0')}:${timeRemainingBits.seconds.toString().padStart(2, '0')}`;
if (timeRemainingBits.hours > 0) {
timeStr = `${timeRemainingBits.hours.toString().padStart(2, '0')}:` + timeStr;
}
const basicTimeLabel = document.getElementById('basicTimeLabel');
const animationTimeLabel = document.getElementById('animationTimeLabel');
// Basic mode digit-change animation
if (basicTimeLabel) {
basicTimeLabel._prevText = timeStr;
basicTimeLabel.textContent = timeStr;
}
if (animationTimeLabel) animationTimeLabel.textContent = timeStr;
// Progress calculation (shared by basic & animation)
const maxMs = startTimeSettings[currentMode];
let currentMs = remainingTimeMs;
if (isRunning && targetDate) {
currentMs = Math.max(0, targetDate.getTime() - Date.now());
}
const progress = 1 - (currentMs / maxMs);
// Basic mode progress bar
const basicProgressBar = document.getElementById('basicProgressBar');
if (basicProgressBar) {
basicProgressBar.style.width = (progress * 100) + '%';
}
// Animation Mode Ring
const ring = document.getElementById('animationProgressRing');
if (ring) {
const circumference = 2 * Math.PI * 150;
ring.style.strokeDasharray = circumference;
ring.style.strokeDashoffset = circumference * (1 - progress);
}
// Animation Mode Hamster orbit along circle
const circleHamster = document.getElementById('circleHamster');
if (circleHamster) {
// Keep hamster centered at top (or middle depending on CSS)
// circleHamster.style.transform = `none`;
// Pause/unpause hamster running animation
if (!isRunning) {
circleHamster.classList.add('paused');
} else {
circleHamster.classList.remove('paused');
}
}
return timeRemainingBits.complete;
}
// Timer Controls
function toggleTimer() {
if (isRunning) {
// Pause
clearInterval(countdownTimer);
isRunning = false;
remainingTimeMs = targetDate.getTime() - Date.now();
if (remainingTimeMs < 0) remainingTimeMs = 0;
startBtn.textContent = 'START';
setAnimalAnimationPaused(true);
} else {
// Start
if (remainingTimeMs <= 0) return;
targetDate = new Date(Date.now() + remainingTimeMs);
isRunning = true;
startBtn.textContent = 'PAUSE';
// Track Start Clicks
if (typeof trackStartClick === 'function') {
trackStartClick();
}
setAnimalAnimationPaused(false);
updateAllSegments();
countdownTimer = setInterval(() => {
const isComplete = updateAllSegments();
const maxMs = startTimeSettings[currentMode];
const currentRemaining = Math.max(0, targetDate.getTime() - Date.now());
const p = Math.max(0, Math.min(100, ((maxMs - currentRemaining) / maxMs) * 100));
const pb = document.getElementById('trackerProgressBar');
if (pb) pb.style.width = p + '%';
if (isComplete) {
clearInterval(countdownTimer);
isRunning = false;
remainingTimeMs = 0;
startBtn.textContent = 'START';
setAnimalAnimationPaused(true);
const pb = document.getElementById('trackerProgressBar');
if (pb) pb.style.width = '0%';
if (currentMode === 'pomodoro' && typeof finishPomodoroSession === 'function') {
finishPomodoroSession();
}
}
}, 1000);
}
}
function resetTimer() {
if (countdownTimer) clearInterval(countdownTimer);
isRunning = false;
remainingTimeMs = startTimeSettings[currentMode];
targetDate = null;
startBtn.textContent = 'START';
updateAllSegments();
setAnimalAnimationPaused(true);
const pb = document.getElementById('trackerProgressBar');
if (pb) pb.style.width = '0%';
}
startBtn.addEventListener('click', toggleTimer);
const userResetBtn = document.getElementById('resetBtn');
if (userResetBtn) {
userResetBtn.addEventListener('click', resetTimer);
}
// Display Mode Logic
let currentDisplayMode = 'flip';
const displayModeBtn = document.getElementById('displayModeBtn');
const displayModeMenu = document.getElementById('displayModeMenu');
const displayModeItems = document.querySelectorAll('.display-mode-item');
const flipClockDisplay = document.getElementById('flipClockDisplay');
const basicClockDisplay = document.getElementById('basicClockDisplay');
const animationClockDisplay = document.getElementById('animationClockDisplay');
if (displayModeBtn) {
displayModeBtn.addEventListener('click', (e) => {
e.stopPropagation();
displayModeMenu.classList.toggle('hidden');
});
}
document.addEventListener('click', (e) => {
if (displayModeMenu && !displayModeMenu.classList.contains('hidden') && !e.target.closest('#displayModeBtn')) {
displayModeMenu.classList.add('hidden');
}
});
displayModeItems.forEach(item => {
item.addEventListener('click', () => {
const mode = item.getAttribute('data-display');
currentDisplayMode = mode;
displayModeItems.forEach(i => i.classList.remove('active'));
item.classList.add('active');
if (flipClockDisplay) flipClockDisplay.classList.add('hidden');
if (basicClockDisplay) basicClockDisplay.classList.add('hidden');
if (animationClockDisplay) animationClockDisplay.classList.add('hidden');
if (mode === 'flip' && flipClockDisplay) flipClockDisplay.classList.remove('hidden');
if (mode === 'basic' && basicClockDisplay) basicClockDisplay.classList.remove('hidden');
if (mode === 'animation') {
if (animationClockDisplay) animationClockDisplay.classList.remove('hidden');
document.body.classList.add('animation-mode');
} else {
document.body.classList.remove('animation-mode');
}
displayModeMenu.classList.add('hidden');
// Save to localStorage
try { localStorage.setItem('pomodoroDisplayMode', mode); } catch (e) { }
});
});
// Mode Selection
modeBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Styling
modeBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Logic
currentMode = btn.getAttribute('data-mode');
resetTimer();
});
});
// Settings & Toggles
const settingsModal = document.getElementById('settingsModal');
const settingsToggle = document.getElementById('settingsToggle');
const closeSettings = document.getElementById('closeSettings');
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
const showHoursToggle = document.getElementById('showHoursToggle');
if (settingsToggle) settingsToggle.addEventListener('click', () => settingsModal.classList.remove('hidden'));
if (closeSettings) closeSettings.addEventListener('click', () => settingsModal.classList.add('hidden'));
if (saveSettingsBtn) saveSettingsBtn.addEventListener('click', () => {
// Update Times
startTimeSettings.pomodoro = parseInt(document.getElementById('setPomodoro').value) * 60 * 1000;
startTimeSettings.shortBreak = parseInt(document.getElementById('setShort').value) * 60 * 1000;
startTimeSettings.longBreak = parseInt(document.getElementById('setLong').value) * 60 * 1000;
// Update Colors & Fonts
document.documentElement.style.setProperty('--bg-color', document.getElementById('bgColor').value);
document.documentElement.style.setProperty('--theme-color', document.getElementById('themeColor').value);
document.documentElement.style.setProperty('--segment-bg', document.getElementById('segmentBgColor').value);
document.documentElement.style.setProperty('--font-family', document.getElementById('fontPicker').value);
// Hours Visibility
const hoursSection = document.getElementById('hours');
const hoursColon = document.getElementById('hoursColon');
if (showHoursToggle && showHoursToggle.checked) {
hoursSection.classList.remove('hidden');
hoursColon.classList.remove('hidden');
} else {
hoursSection.classList.add('hidden');
hoursColon.classList.add('hidden');
}
settingsModal.classList.add('hidden');
resetTimer();
});
// Reset Settings to Defaults
const resetSettingsBtn = document.getElementById('resetSettingsBtn');
if (resetSettingsBtn) resetSettingsBtn.addEventListener('click', () => {
// Default values
const defaults = {
pomodoro: 25, shortBreak: 5, longBreak: 15,
bgColor: '#121212', themeColor: '#ffffff',
segmentBgColor: '#000000', fontFamily: 'Inter, sans-serif',
trackerScale: 1
};
// Reset inputs
document.getElementById('setPomodoro').value = defaults.pomodoro;
document.getElementById('setShort').value = defaults.shortBreak;
document.getElementById('setLong').value = defaults.longBreak;
document.getElementById('bgColor').value = defaults.bgColor;
document.getElementById('themeColor').value = defaults.themeColor;
document.getElementById('segmentBgColor').value = defaults.segmentBgColor;
document.getElementById('fontPicker').value = defaults.fontFamily;
const trackerSlider = document.getElementById('trackerSizeSlider');
if (trackerSlider) trackerSlider.value = defaults.trackerScale;
// Apply CSS variables
document.documentElement.style.setProperty('--bg-color', defaults.bgColor);
document.documentElement.style.setProperty('--theme-color', defaults.themeColor);
document.documentElement.style.setProperty('--segment-bg', defaults.segmentBgColor);
document.documentElement.style.setProperty('--font-family', defaults.fontFamily);
document.documentElement.style.setProperty('--tracker-scale', defaults.trackerScale);
// Reset times
startTimeSettings.pomodoro = defaults.pomodoro * 60 * 1000;
startTimeSettings.shortBreak = defaults.shortBreak * 60 * 1000;
startTimeSettings.longBreak = defaults.longBreak * 60 * 1000;
resetTimer();
});
// Spotify Widget
const spotifyWidget = document.getElementById('spotifyWidget');
const spotifyToggle = document.getElementById('spotifyToggle');
const closeSpotify = document.getElementById('closeSpotify');
const loadSpotifyBtn = document.getElementById('loadSpotifyBtn');
const spotifyLink = document.getElementById('spotifyLink');
const spotifyIframe = document.getElementById('spotifyIframe');
if (spotifyToggle) spotifyToggle.addEventListener('click', () => spotifyWidget.classList.toggle('hidden'));
if (closeSpotify) closeSpotify.addEventListener('click', () => spotifyWidget.classList.add('hidden'));
if (loadSpotifyBtn) loadSpotifyBtn.addEventListener('click', () => {
const rawUrl = spotifyLink.value.trim();
if (!rawUrl) return;
let embedUrl = rawUrl;
if (rawUrl.includes('open.spotify.com')) {
// Strip ?si= and other params; build clean embed URL with dark theme
const match = rawUrl.match(/open\.spotify\.com\/(embed\/)?(playlist|album|track|episode|show)\/([a-zA-Z0-9]+)/);
if (match) {
embedUrl = `https://open.spotify.com/embed/${match[2]}/${match[3]}?utm_source=generator&theme=0`;
}
}
spotifyIframe.src = embedUrl;
});
// Init
updateAllSegments();
// Restore display mode from localStorage
(function restoreDisplayMode() {
try {
const saved = localStorage.getItem('pomodoroDisplayMode');
if (saved && ['flip', 'basic', 'animation'].includes(saved)) {
currentDisplayMode = saved;
displayModeItems.forEach(i => {
i.classList.toggle('active', i.getAttribute('data-display') === saved);
});
if (flipClockDisplay) flipClockDisplay.classList.add('hidden');
if (basicClockDisplay) basicClockDisplay.classList.add('hidden');
if (animationClockDisplay) animationClockDisplay.classList.add('hidden');
if (saved === 'flip' && flipClockDisplay) flipClockDisplay.classList.remove('hidden');
if (saved === 'basic' && basicClockDisplay) basicClockDisplay.classList.remove('hidden');
if (saved === 'animation') {
if (animationClockDisplay) animationClockDisplay.classList.remove('hidden');
document.body.classList.add('animation-mode');
} else {
document.body.classList.remove('animation-mode');
}
}
} catch (e) { }
})();
// Animal Switcher Logic
function setAnimalAnimationPaused(isPaused) {
const activeAnim = document.querySelector('.animal-animation:not(.hidden)');
if (activeAnim) {
if (isPaused) {
activeAnim.classList.add('paused');
} else {
activeAnim.classList.remove('paused');
}
}
const circleHamster = document.getElementById('circleHamster');
if (circleHamster) {
if (isPaused) {
circleHamster.classList.add('paused');
} else {
circleHamster.classList.remove('paused');
}
}
}
const animals = [
{ id: 'hamsterAnim', name: 'BigBike' },
{ id: 'turtlesAnim', name: 'Heng and Dee' },
{ id: 'dogAnim', name: 'Meenam' },
{ id: 'capybaraAnim', name: 'FeiFei' }
];
let currentAnimalIndex = 0;
const animalSwitcherBtn = document.getElementById('animalSwitcher');
const animalNameLabel = document.getElementById('animalName');
const animalMenu = document.getElementById('animalSelectionMenu');
const animalCards = document.querySelectorAll('.animal-card');
if (animalSwitcherBtn) {
animalSwitcherBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (animalMenu) animalMenu.classList.toggle('hidden');
});
}
document.addEventListener('click', (e) => {
if (animalMenu && !animalMenu.classList.contains('hidden')) {
if (!animalMenu.contains(e.target) && animalSwitcherBtn && !animalSwitcherBtn.contains(e.target)) {
animalMenu.classList.add('hidden');
}
}
});
if (animalCards.length > 0) {
animalCards.forEach(card => {
card.addEventListener('click', () => {
const index = parseInt(card.getAttribute('data-animal-index'));
if (isNaN(index)) return;
// Hide menu
if (animalMenu) animalMenu.classList.add('hidden');
// Hide current
const currentAnim = document.getElementById(animals[currentAnimalIndex].id);
if (currentAnim) {
currentAnim.classList.add('hidden');
currentAnim.classList.add('paused');
}
// Set index
currentAnimalIndex = index;
// Show next
const nextAnim = document.getElementById(animals[currentAnimalIndex].id);
if (nextAnim) {
nextAnim.classList.remove('hidden');
// sync pause state
setAnimalAnimationPaused(!isRunning);
}
// Update label
if (animalNameLabel) {
animalNameLabel.textContent = animals[currentAnimalIndex].name;
}
});
});
}
// ==========================================
// PET CLOSE / SHOW & DRAG LEFT
// ==========================================
const trackerWrapper = document.querySelector('.task-tracker-wrapper');
const petCloseBtn = document.getElementById('petCloseBtn');
const showPetBtn = document.getElementById('showPetBtn');
if (petCloseBtn && trackerWrapper && showPetBtn) {
petCloseBtn.addEventListener('click', (e) => {
e.stopPropagation();
trackerWrapper.classList.add('pet-hidden');
// Close animal menu too
if (animalMenu) animalMenu.classList.add('hidden');
// Show the floating paw button after transition
setTimeout(() => {
showPetBtn.classList.remove('hidden');
}, 300);
});
showPetBtn.addEventListener('click', () => {
showPetBtn.classList.add('hidden');
trackerWrapper.classList.remove('pet-hidden');
});
}
// Drag tracker widget horizontally (to the left)
if (trackerWrapper) {
let isDraggingWidget = false;
let widgetDragStartX = 0;
let widgetDragStartY = 0;
let widgetStartRight = 20;
let widgetStartBottom = 20;
let wasDragged = false;
function getWidgetPosition() {
const style = window.getComputedStyle(trackerWrapper);
const right = parseInt(style.right) || 20;
const bottom = parseInt(style.bottom) || 20;
return { right, bottom };
}
function startWidgetDrag(clientX, clientY) {
isDraggingWidget = true;
wasDragged = false;
widgetDragStartX = clientX;
widgetDragStartY = clientY;
const pos = getWidgetPosition();
widgetStartRight = pos.right;
widgetStartBottom = pos.bottom;
trackerWrapper.style.transition = 'none';
}
function moveWidgetDrag(clientX, clientY) {
if (!isDraggingWidget) return;
const deltaX = clientX - widgetDragStartX;
const deltaY = clientY - widgetDragStartY;
if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
wasDragged = true;
}
// Moving left = increasing right offset (since widget is right-anchored)
let newRight = widgetStartRight - deltaX;
let newBottom = widgetStartBottom - deltaY;
// Clamp to viewport
const maxRight = window.innerWidth - 100;
const maxBottom = window.innerHeight - 100;
newRight = Math.max(-20, Math.min(maxRight, newRight));
newBottom = Math.max(10, Math.min(maxBottom, newBottom));
trackerWrapper.style.right = newRight + 'px';
trackerWrapper.style.bottom = newBottom + 'px';
}
function endWidgetDrag() {
if (!isDraggingWidget) return;
isDraggingWidget = false;
trackerWrapper.style.transition = '';
}
// Mouse events for drag
const hamsterContainer = trackerWrapper.querySelector('.hamster-container');
if (hamsterContainer) {
hamsterContainer.addEventListener('mousedown', (e) => {
if (e.target.closest('.animal-controls') || e.target.closest('button')) return;
e.preventDefault();
startWidgetDrag(e.clientX, e.clientY);
});
// Touch events for drag
hamsterContainer.addEventListener('touchstart', (e) => {
if (e.target.closest('.animal-controls') || e.target.closest('button')) return;
if (e.touches.length !== 1) return;
startWidgetDrag(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: true });
}
document.addEventListener('mousemove', (e) => {
if (isDraggingWidget) moveWidgetDrag(e.clientX, e.clientY);
});
document.addEventListener('mouseup', () => endWidgetDrag());
document.addEventListener('touchmove', (e) => {
if (isDraggingWidget && e.touches.length === 1) {
moveWidgetDrag(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: true });
document.addEventListener('touchend', () => endWidgetDrag());
}
// ==========================================
// Task Planner Logic (Pomofocus Style)
// ==========================================
let tasks = JSON.parse(localStorage.getItem('pomodoroTasks')) || [];
let activeTaskId = localStorage.getItem('pomodoroActiveTaskId') || null;
const taskPanel = document.getElementById('taskPanel');
const taskTabBtn = document.getElementById('taskTabBtn');
const closeTaskPanel = document.getElementById('closeTaskPanel');
const taskList = document.getElementById('taskList');
const addTaskTriggerBtn = document.getElementById('addTaskTriggerBtn');
const addTaskForm = document.getElementById('addTaskForm');
const cancelAddTaskBtn = document.getElementById('cancelAddTaskBtn');
const saveTaskBtn = document.getElementById('saveTaskBtn');
const taskTitleInput = document.getElementById('taskTitleInput');
const taskEstInput = document.getElementById('taskEstInput');
const estUpBtn = document.getElementById('estUpBtn');
const estDownBtn = document.getElementById('estDownBtn');
// Toggle Planner
if (taskTabBtn) taskTabBtn.addEventListener('click', () => taskPanel.classList.toggle('hidden'));
if (closeTaskPanel) closeTaskPanel.addEventListener('click', () => taskPanel.classList.add('hidden'));
// Add Task Form Toggles
if (addTaskTriggerBtn) addTaskTriggerBtn.addEventListener('click', () => {
addTaskForm.classList.remove('hidden');
addTaskTriggerBtn.classList.add('hidden');
taskTitleInput.focus();
});
if (cancelAddTaskBtn) cancelAddTaskBtn.addEventListener('click', () => {
addTaskForm.classList.add('hidden');
addTaskTriggerBtn.classList.remove('hidden');
taskTitleInput.value = '';
taskEstInput.value = '1';
});
// Est Input buttons
if (estUpBtn) estUpBtn.addEventListener('click', () => { taskEstInput.value = parseInt(taskEstInput.value || 0) + 1; });
if (estDownBtn) estDownBtn.addEventListener('click', () => { taskEstInput.value = Math.max(1, parseInt(taskEstInput.value || 0) - 1); });
function saveTasks() {
localStorage.setItem('pomodoroTasks', JSON.stringify(tasks));
if (activeTaskId) localStorage.setItem('pomodoroActiveTaskId', activeTaskId);
else localStorage.removeItem('pomodoroActiveTaskId');
renderTasks();
updateActiveTaskDisplay();
}
if (saveTaskBtn) saveTaskBtn.addEventListener('click', () => {
const title = taskTitleInput.value.trim();
if (!title) return;
const newTask = {
id: Date.now().toString(),
title: title,
est: parseInt(taskEstInput.value) || 1,
completed: 0,
done: false
};
tasks.push(newTask);
if (!activeTaskId) activeTaskId = newTask.id; // Auto select if none
saveTasks();
cancelAddTaskBtn.click();
});
// Increment active task pomodoros (Called from Timer logic)
window.finishPomodoroSession = function () {
// 1. Alert Sound
const alertEnabled = document.getElementById('alertEnableToggle') ? document.getElementById('alertEnableToggle').checked : true;
if (alertEnabled && window.playAlertSound) {
window.playAlertSound();
}
// 2. Stats Tracking
if (window.incrementSessionStat) {
window.incrementSessionStat();
}
// 3. Update active task and auto-advance
if (activeTaskId) {
const taskIndex = tasks.findIndex(t => t.id === activeTaskId);
if (taskIndex !== -1) {
tasks[taskIndex].completed += 1;
// Auto advance if completed meets est (or we can just mark done if completed >= est, but user requested advance when task is completed)
// Let's just assume task is marked done, and advance to next undone task
tasks[taskIndex].done = true;
// Find next undone
const nextUndone = tasks.find((t, idx) => idx > taskIndex && !t.done) || tasks.find(t => !t.done);
if (nextUndone) {
activeTaskId = nextUndone.id;
} else {
activeTaskId = null; // No tasks left
}
saveTasks();
}
}
updateActiveTaskDisplay();
};
function updateActiveTaskDisplay() {
const displayEl = document.getElementById('activeTaskText');
if (!displayEl) return;
let task = null;
if (activeTaskId) {
task = tasks.find(t => t.id === activeTaskId && !t.done);
}
if (!task && tasks.length > 0) {
task = tasks.find(t => !t.done);
if (task) {
activeTaskId = task.id;
}
}
if (task) {
const taskText = task.text || task.title || task.name || task.content || "";
displayEl.textContent = taskText;
return;
}
const phrases = [
"What do you want to focus on?",
"Continue your flow?",
"Ready for the next deep work block?",
"Keep the momentum alive.",
"Stay locked in.",
"One more focused step.",
"Your next win starts now."
];
// Prevent random phrase flashing on every render if we already have a valid phrase
if (!phrases.includes(displayEl.textContent)) {
displayEl.textContent = phrases[Math.floor(Math.random() * phrases.length)];
}
}
function selectTask(id) {
activeTaskId = id;
saveTasks();
}
function toggleTaskDone(id, event) {
event.stopPropagation();
const task = tasks.find(t => t.id === id);
if (task) {
task.done = !task.done;
saveTasks();
}
}
function renderTasks() {
if (!taskList) return;
taskList.innerHTML = '';
tasks.forEach(task => {
const item = document.createElement('div');
item.className = 'task-item' + (task.id === activeTaskId ? ' active' : '') + (task.done ? ' done' : '');
item.onclick = () => selectTask(task.id);
const header = document.createElement('div');
header.className = 'task-header';
const checkbox = document.createElement('div');
checkbox.className = 'task-checkbox' + (task.done ? ' checked' : '');
checkbox.innerHTML = task.done ? '' : '';
checkbox.onclick = (e) => toggleTaskDone(task.id, e);
const title = document.createElement('div');
title.className = 'task-title';
title.textContent = task.title;
const ticks = document.createElement('div');
ticks.className = 'task-ticks';
ticks.textContent = `${task.completed} / ${task.est}`;
const headerLeft = document.createElement('div');
headerLeft.className = 'task-header-left';
headerLeft.appendChild(checkbox);
headerLeft.appendChild(title);
const headerRight = document.createElement('div');
headerRight.className = 'task-header-right';
headerRight.appendChild(ticks);
const trashIcon = document.createElement('i');
trashIcon.className = 'fas fa-trash-alt task-delete-btn';
trashIcon.onclick = (e) => {
e.stopPropagation();
tasks = tasks.filter(t => t.id !== task.id);
if (activeTaskId === task.id) activeTaskId = null;
saveTasks();
renderTasks();
};
headerRight.appendChild(trashIcon);
header.appendChild(headerLeft);
header.appendChild(headerRight);
item.appendChild(header);
taskList.appendChild(item);
});
}
// Initial render
renderTasks();
updateActiveTaskDisplay();
// ==========================================
// UI Customizations (Scale & Position)
// ==========================================
// Tracker Widget Scale
const trackerSizeSlider = document.getElementById('trackerSizeSlider');
if (trackerSizeSlider) {
trackerSizeSlider.addEventListener('input', (e) => {
document.documentElement.style.setProperty('--tracker-scale', e.target.value);
});
}
// Draggable Task Tab
if (taskTabBtn) {
let isDraggingTab = false;
let startY = 0;
let startTop = 0;
taskTabBtn.addEventListener('mousedown', (e) => {
// Only trigger drag if we don't consider it a click right away, but mousedown works.
isDraggingTab = true;
startY = e.clientY;
const inlineTop = taskTabBtn.style.getPropertyValue('--tab-y');
startTop = parseInt(inlineTop) || 10;
// Don't prevent default completely here or the click won't fire.
// Or handle drag logic smartly so click still works safely.
});
document.addEventListener('mousemove', (e) => {
if (!isDraggingTab) return;
const deltaY = e.clientY - startY;
// Add a threshold before considering it a 'drag' to preserve clicks
if (Math.abs(deltaY) > 2) {
let newTop = startTop + deltaY;
const mainHeight = document.querySelector('.tracker-main')?.clientHeight || 260;
const tabHeight = taskTabBtn.clientHeight || 80;
// Allow bounds
if (newTop < 0) newTop = 0;
if (newTop > mainHeight - tabHeight + 20) newTop = mainHeight - tabHeight + 20;
taskTabBtn.style.setProperty('--tab-y', `${newTop}px`);
}
});
document.addEventListener('mouseup', () => {
isDraggingTab = false;
});
}
// ==========================================
// NOTION-STYLE CALENDAR & EVENT SYSTEM (PHASE 2)
// ==========================================
const calendarToggle = document.getElementById('calendarToggle');
const calendarOverlay = document.getElementById('calendarOverlay');
const closeCalendarBtn = document.getElementById('closeCalendarBtn');
const calDaysHeader = document.getElementById('calDaysHeader');
const calTimeCol = document.getElementById('calTimeCol');
const calGridLines = document.getElementById('calGridLines');
const calGrid = document.getElementById('calGrid');
const calEventsLayer = document.getElementById('calEventsLayer');
const eventEditorModal = document.getElementById('eventEditorModal');
const closeEventModal = document.getElementById('closeEventModal');
const eventTitleInput = document.getElementById('eventTitleInput');
const saveEventBtn = document.getElementById('saveEventBtn');
const deleteEventBtn = document.getElementById('deleteEventBtn');
const calViewSelect = document.getElementById('calViewSelect');
const calCurrentViewTitle = document.getElementById('calCurrentViewTitle');
const calPrevBtn = document.getElementById('calPrevBtn');
const calTodayBtn = document.getElementById('calTodayBtn');
const calNextBtn = document.getElementById('calNextBtn');
let notionEvents = JSON.parse(localStorage.getItem('notionEvents')) || [];
// Sanitize NaN values from stored events
notionEvents = notionEvents.map(ev => {
if (!ev.title || ev.title === 'NaN' || ev.title === 'undefined') ev.title = '';
if (!ev.notes || ev.notes === 'NaN' || ev.notes === 'undefined') ev.notes = '';
if (isNaN(ev.startHour)) ev.startHour = 9;
if (isNaN(ev.duration) || ev.duration <= 0) ev.duration = 1;
return ev;
});
let currentEditingEventId = null;
let newEventDateStr = '';
let newEventHour = 0;
let currentCalView = 'week'; // day, week, month
let currentDate = new Date();
let calHourHeight = 60; // Global hour height for zoom
function updateCalendarTitle() {
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
if (currentCalView === 'month') {
calCurrentViewTitle.textContent = `${months[currentDate.getMonth()]} ${currentDate.getFullYear()}`;
} else if (currentCalView === 'day') {
calCurrentViewTitle.textContent = `${months[currentDate.getMonth()]} ${currentDate.getDate()}, ${currentDate.getFullYear()}`;
} else {
calCurrentViewTitle.textContent = `Week of ${months[currentDate.getMonth()]} ${currentDate.getFullYear()}`;
}
}
function initCalendarUI() {
updateCalendarTitle();
calDaysHeader.innerHTML = '
';
calTimeCol.innerHTML = '';
calGrid.className = 'cal-grid'; // reset
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
// Helper: 0 for Monday, 6 for Sunday
const getMonIndex = (d) => (d.getDay() + 6) % 7;
if (currentCalView === 'month') {
calGrid.classList.add('month-view-grid');
calDaysHeader.classList.add('month-header');
calDaysHeader.innerHTML = '';
days.forEach(d => {
calDaysHeader.innerHTML += `${d}
`;
});
// simple 35-block month grid
calGrid.innerHTML = '';
let tempDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
let startDay = getMonIndex(tempDate);
tempDate.setDate(tempDate.getDate() - startDay); // roll back to Monday
for (let i = 0; i < 35; i++) {
const isToday = tempDate.toDateString() === new Date().toDateString();
const dateS = getLocalDateKey(tempDate);
calGrid.innerHTML += `
`;
tempDate.setDate(tempDate.getDate() + 1);
}
} else {
// Day / Week view
calDaysHeader.classList.remove('month-header');
const numCols = currentCalView === 'day' ? 1 : 7;
calGrid.style.gridTemplateColumns = `repeat(${numCols}, 1fr)`;
let startD = new Date(currentDate);
if (currentCalView === 'week') {
startD.setDate(startD.getDate() - getMonIndex(startD));
}
for (let i = 0; i < numCols; i++) {
const isToday = startD.toDateString() === new Date().toDateString();
calDaysHeader.innerHTML += `
${days[getMonIndex(startD)]} ${startD.getDate()}
`;
startD.setDate(startD.getDate() + 1);
}
for (let i = 0; i < 24; i++) {
let label = i === 0 ? '12 AM' : i < 12 ? i + ' AM' : i === 12 ? '12 PM' : (i - 12) + ' PM';
calTimeCol.innerHTML += `${label}
`;
}
// Restore elements
calGrid.innerHTML = `
`;
}
renderEvents();
}
function renderEvents() {
if (currentCalView === 'month') {
const containers = document.querySelectorAll('.month-events-container');
containers.forEach(c => c.innerHTML = '');
notionEvents.forEach(ev => {
if (!ev.dateStr) return;
const container = document.getElementById(`month-ev-${ev.dateStr}`);
if (container) {
const block = document.createElement('div');
block.className = 'month-event-dot';
block.textContent = ev.title || 'Untitled';
block.addEventListener('click', (e) => {
e.stopPropagation();
openEventModal(ev.id);
});
container.appendChild(block);
}
});
return;
}
const layer = document.getElementById('calEventsLayer');
if (!layer) return;
layer.innerHTML = '';
const numCols = currentCalView === 'day' ? 1 : 7;
const colWidth = 100 / numCols;
const hourHeight = calHourHeight;
let startD = new Date(currentDate);
if (currentCalView === 'week') {
const getMonIndexLocal = (d) => (d.getDay() + 6) % 7;
startD.setDate(startD.getDate() - getMonIndexLocal(startD));
}
const visibleDates = [];
for (let i = 0; i < numCols; i++) {
visibleDates.push(getLocalDateKey(startD));
startD.setDate(startD.getDate() + 1);
}
notionEvents.forEach(ev => {
let evDateStr = ev.dateStr;
if (!evDateStr && ev.dayIndex !== undefined) {
evDateStr = visibleDates[ev.dayIndex] || visibleDates[0];
ev.dateStr = evDateStr;
}
const colIndex = visibleDates.indexOf(evDateStr);
if (colIndex === -1) return;
const block = document.createElement('div');
block.className = 'cal-event-block';
block.style.top = (ev.startHour * hourHeight) + 'px';
block.style.height = (ev.duration * hourHeight) + 'px';
block.style.left = (colIndex * colWidth) + '%';
block.style.width = colWidth + '%';
block.innerHTML = `
${av(ev.title, 'No Title')}
`;
// Dragging & Resizing Physics (Mouse)
block.addEventListener('mousedown', (e) => startEventInteraction(e, block, ev, visibleDates));
// Dragging & Resizing Physics (Touch)
block.addEventListener('touchstart', (e) => {
if (e.touches.length !== 1) return;
const touch = e.touches[0];
const fakeEvent = {
clientX: touch.clientX,
clientY: touch.clientY,
target: document.elementFromPoint(touch.clientX, touch.clientY) || e.target,
stopPropagation: () => e.stopPropagation(),
preventDefault: () => e.preventDefault()
};
startEventInteraction(fakeEvent, block, ev, visibleDates);
}, { passive: false });
layer.appendChild(block);
});
}
// Interactive dragging
let isDraggingEvent = false;
let isResizingEvent = false;
let activeDragEventId = null;
let activeDragBlock = null;
let dragStartY = 0;
let dragStartX = 0;
let dragStartTop = 0;
let dragStartLeftRaw = '';
let dragStartHeight = 0;
let currentVisibleDatesForDrag = [];
function startEventInteraction(e, block, ev, visibleDates) {
e.stopPropagation();
activeDragEventId = ev.id;
activeDragBlock = block;
dragStartY = e.clientY;
dragStartX = e.clientX;
dragStartTop = parseFloat(block.style.top);
dragStartLeftRaw = block.style.left;
dragStartHeight = parseFloat(block.style.height);
currentVisibleDatesForDrag = visibleDates;
if (e.target.classList.contains('event-resize-handle')) {
isResizingEvent = true;
} else {
isDraggingEvent = true;
block.classList.add('dragging');
}
}
document.addEventListener('mousemove', (e) => {
if ((isDraggingEvent || isResizingEvent) && activeDragBlock) {
handleEventDragMove(e.clientX, e.clientY);
}
});
document.addEventListener('mouseup', (e) => {
finishEventInteraction(e);
});
// Touch move/end for event dragging
document.addEventListener('touchmove', (e) => {
if ((isDraggingEvent || isResizingEvent) && activeDragBlock && e.touches.length === 1) {
e.preventDefault();
const touch = e.touches[0];
handleEventDragMove(touch.clientX, touch.clientY);
}
}, { passive: false });
document.addEventListener('touchend', (e) => {
if (isDraggingEvent || isResizingEvent) {
const touch = e.changedTouches[0];
const fakeEvent = {
clientX: touch.clientX,
clientY: touch.clientY
};
finishEventInteraction(fakeEvent);
}
});
function handleEventDragMove(clientX, clientY) {
if (isDraggingEvent && activeDragBlock) {
let deltaY = clientY - dragStartY;
let deltaX = clientX - dragStartX;
let newTop = Math.max(0, dragStartTop + deltaY);
let snapUnit = calHourHeight / 4;
newTop = Math.round(newTop / snapUnit) * snapUnit;
activeDragBlock.style.top = newTop + 'px';
if (currentCalView === 'week') {
const gridRect = document.getElementById('calGrid').getBoundingClientRect();
let newX = dragStartX + deltaX - gridRect.left;
let colIndex = Math.floor(newX / (gridRect.width / 7));
colIndex = Math.max(0, Math.min(6, colIndex));
activeDragBlock.style.left = (colIndex * (100 / 7)) + '%';
}
} else if (isResizingEvent && activeDragBlock) {
let deltaY = clientY - dragStartY;
let snapUnit = calHourHeight / 4;
let newHeight = Math.max(snapUnit, dragStartHeight + deltaY);
newHeight = Math.round(newHeight / snapUnit) * snapUnit;
activeDragBlock.style.height = newHeight + 'px';
}
}
function finishEventInteraction(e) {
if ((isDraggingEvent || isResizingEvent) && activeDragBlock && activeDragEventId) {
const ev = notionEvents.find(x => x.id === activeDragEventId);
if (ev) {
ev.startHour = parseFloat(activeDragBlock.style.top) / calHourHeight;
ev.duration = parseFloat(activeDragBlock.style.height) / calHourHeight;
if (isDraggingEvent && currentCalView === 'week') {
const colIndex = Math.round(parseFloat(activeDragBlock.style.left) / (100 / 7));
if (currentVisibleDatesForDrag[colIndex]) {
ev.dateStr = currentVisibleDatesForDrag[colIndex];
}
}
localStorage.setItem('notionEvents', JSON.stringify(notionEvents));
}
activeDragBlock.classList.remove('dragging');
const wasDragged = Math.abs(e.clientY - dragStartY) >= 3 || Math.abs(e.clientX - dragStartX) >= 3;
isDraggingEvent = false;
isResizingEvent = false;
activeDragEventId = null;
activeDragBlock = null;
currentVisibleDatesForDrag = [];
if (!wasDragged && ev) {
openEventModal(ev.id);
} else {
renderEvents();
}
}
}
function av(val, def) {
return val && val.trim() !== '' ? val : def;
}
if (calendarToggle) {
calendarToggle.addEventListener('click', () => {
calendarOverlay.classList.remove('hidden');
initCalendarUI();
});
}
if (closeCalendarBtn) {
closeCalendarBtn.addEventListener('click', () => {
calendarOverlay.classList.add('hidden');
});
}
if (calViewSelect) {
calViewSelect.addEventListener('change', (e) => {
currentCalView = e.target.value;
initCalendarUI();
});
}
if (calPrevBtn) calPrevBtn.addEventListener('click', () => {
if (currentCalView === 'day') currentDate.setDate(currentDate.getDate() - 1);
else if (currentCalView === 'week') currentDate.setDate(currentDate.getDate() - 7);
else currentDate.setMonth(currentDate.getMonth() - 1);
initCalendarUI();
});
if (calNextBtn) calNextBtn.addEventListener('click', () => {
if (currentCalView === 'day') currentDate.setDate(currentDate.getDate() + 1);
else if (currentCalView === 'week') currentDate.setDate(currentDate.getDate() + 7);
else currentDate.setMonth(currentDate.getMonth() + 1);
initCalendarUI();
});
if (calTodayBtn) calTodayBtn.addEventListener('click', () => {
currentDate = new Date();
initCalendarUI();
});
// Click Grid to Create Event
if (calGrid) {
calGrid.addEventListener('click', (e) => {
if (e.target.closest('.cal-event-block') || e.target.closest('.month-event-dot')) return;
if (currentCalView === 'month') {
const monthCell = e.target.closest('.month-day-cell');
if (monthCell) {
newEventDateStr = monthCell.getAttribute('data-datestr');
newEventHour = new Date().getHours() % 24; // default near current time
openEventModal(null);
}
return;
}
const rect = calGrid.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const numCols = currentCalView === 'day' ? 1 : 7;
const dayIndex = Math.floor(clickX / (rect.width / numCols));
const startHour = Math.floor(clickY / calHourHeight);
let targetD = new Date(currentDate);
if (currentCalView === 'week') {
const getMonIndexLocal = (d) => (d.getDay() + 6) % 7;
targetD.setDate(targetD.getDate() - getMonIndexLocal(targetD) + Math.max(0, Math.min(6, dayIndex)));
}
newEventDateStr = getLocalDateKey(targetD);
newEventHour = Math.max(0, Math.min(23, startHour));
openEventModal(null);
});
// Semantic Zoom gestures (Scroll / Trackpad Pinch)
let lastZoomTime = 0;
calGrid.addEventListener('wheel', (e) => {
// Only intercept if we actually want to zoom, throttle to prevent insane scrolling
const now = Date.now();
if (now - lastZoomTime < 400) return;
const views = ['day', 'week', 'month'];
let currentIndex = views.indexOf(currentCalView);
let changed = false;
if (e.deltaY > 20) {
currentIndex = Math.min(2, currentIndex + 1);
changed = true;
} else if (e.deltaY < -20) {
currentIndex = Math.max(0, currentIndex - 1);
changed = true;
}
if (changed && views[currentIndex] !== currentCalView) {
e.preventDefault(); // Only prevent default vertically if we successfully zoomed
currentCalView = views[currentIndex];
calViewSelect.value = currentCalView;
initCalendarUI();
renderEvents();
lastZoomTime = now;
}
}, { passive: false });
}
// ==========================================
// EVENT EDITOR & NOTION TABS
// ==========================================
const eventTabs = document.querySelectorAll('.event-tab-btn');
const tabPanes = document.querySelectorAll('.event-tab-pane');
const eventNotesArea = document.getElementById('eventNotesArea');
const sketchGallery = document.getElementById('sketchGallery');
let currentEventSketches = [];
eventTabs.forEach(btn => {
btn.addEventListener('click', () => {
eventTabs.forEach(b => b.classList.remove('active'));
tabPanes.forEach(p => p.classList.add('hidden'));
btn.classList.add('active');
document.getElementById(btn.getAttribute('data-tab')).classList.remove('hidden');
});
});
let isStandaloneNote = false;
let currentEditingNoteId = null;
let standaloneNotes = JSON.parse(localStorage.getItem('fliqlow_standalone_notes')) || [];
const noteToggleBtn = document.getElementById('noteToggleBtn');
const noteListDropdown = document.getElementById('noteListDropdown');
const createNewNoteBtn = document.getElementById('createNewNoteBtn');
const noteListContainer = document.getElementById('noteListContainer');
if (noteToggleBtn && noteListDropdown) {
noteToggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
// Hide other dropdowns if needed
const ambienceDrawer = document.getElementById('ambienceDrawer');
if (ambienceDrawer && !ambienceDrawer.classList.contains('hidden')) {
ambienceDrawer.classList.add('hidden');
}
noteListDropdown.classList.toggle('hidden');
renderNoteList();
});
document.addEventListener('click', (e) => {
if (!e.target.closest('#notePanelContainer') && !e.target.closest('#eventEditorModal')) {
noteListDropdown.classList.add('hidden');
}
});
}
if (createNewNoteBtn) {
createNewNoteBtn.addEventListener('click', (e) => {
e.stopPropagation();
noteListDropdown.classList.add('hidden');
openEventModal(null, true);
});
}
function renderNoteList() {
if (!noteListContainer) return;
noteListContainer.innerHTML = '';
if (standaloneNotes.length === 0) {
noteListContainer.innerHTML = 'No notes yet. Click + to create one.
';
return;
}
// Sort by updated descending
const sortedNotes = [...standaloneNotes].sort((a, b) => b.updatedAt - a.updatedAt);
sortedNotes.forEach(note => {
const item = document.createElement('div');
item.style.padding = '8px';
item.style.background = 'rgba(255,255,255,0.05)';
item.style.borderRadius = '6px';
item.style.cursor = 'pointer';
item.style.border = '1px solid rgba(255,255,255,0.1)';
// Preview text (strip HTML)
const tempDiv = document.createElement('div');
tempDiv.innerHTML = note.content || '';
let previewText = tempDiv.textContent || tempDiv.innerText || '';
if (previewText.length > 40) previewText = previewText.substring(0, 40) + '...';
if (!previewText.trim()) previewText = 'No content';
item.innerHTML = `
${note.title || 'Untitled'}
${previewText}
${new Date(note.updatedAt).toLocaleDateString()} ${new Date(note.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
`;
item.addEventListener('click', () => {
noteListDropdown.classList.add('hidden');
openEventModal(note.id, true);
});
noteListContainer.appendChild(item);
});
}
function openEventModal(eventId, isNote = false) {
isStandaloneNote = isNote;
eventEditorModal.classList.remove('hidden');
if (isStandaloneNote) {
currentEditingNoteId = eventId;
currentEditingEventId = null;
} else {
currentEditingEventId = eventId;
currentEditingNoteId = null;
}
// Reset standard UI
eventTitleInput.value = '';
eventNotesArea.innerHTML = '';
currentEventSketches = [];
// Reset tabs
eventTabs[0].click();
if (isStandaloneNote) {
if (eventId) {
const note = standaloneNotes.find(n => n.id === eventId);
if (note) {
eventTitleInput.value = note.title || '';
let safeNotes = (note.content && note.content !== 'NaN') ? note.content : '';
safeNotes = safeNotes.replace(/>NaN<');
safeNotes = safeNotes.replace(/^NaN$/, '');
eventNotesArea.innerHTML = safeNotes;
currentEventSketches = note.sketches ? [...note.sketches] : [];
document.getElementById('eventModalSubtitle').textContent = 'Edit Note';
if (deleteEventBtn) deleteEventBtn.classList.remove('hidden');
if (eventNotesArea.innerHTML.trim() !== '') {
const hasBlocks = Array.from(eventNotesArea.children).some(c => c.classList && (c.classList.contains('notion-block') || c.classList.contains('notion-todo-item')));
if (!hasBlocks) {
const wrapper = document.createElement('div');
wrapper.className = 'notion-block';
wrapper.contentEditable = 'true';
wrapper.innerHTML = eventNotesArea.innerHTML;
eventNotesArea.innerHTML = '';
eventNotesArea.appendChild(wrapper);
}
}
ensureDefaultBlock();
}
} else {
document.getElementById('eventModalSubtitle').textContent = 'New Note';
if (deleteEventBtn) deleteEventBtn.classList.add('hidden');
ensureDefaultBlock();
setTimeout(() => eventTitleInput.focus(), 100);
}
} else {
if (eventId) {
const ev = notionEvents.find(e => e.id === eventId);
if (ev) {
eventTitleInput.value = (ev.title && ev.title !== 'NaN') ? ev.title : '';
let safeNotes = (ev.notes && ev.notes !== 'NaN') ? ev.notes : '';
safeNotes = safeNotes.replace(/>NaN<');
safeNotes = safeNotes.replace(/^NaN$/, '');
eventNotesArea.innerHTML = safeNotes;
currentEventSketches = ev.sketches ? [...ev.sketches] : [];
document.getElementById('eventModalSubtitle').textContent = 'Edit Event';
if (deleteEventBtn) deleteEventBtn.classList.remove('hidden');
// SANITIZE: Wrap raw text in blocks to fix "unavailable" notes issue
if (eventNotesArea.innerHTML.trim() !== '') {
const hasBlocks = Array.from(eventNotesArea.children).some(c => c.classList && (c.classList.contains('notion-block') || c.classList.contains('notion-todo-item')));
if (!hasBlocks) {
const wrapper = document.createElement('div');
wrapper.className = 'notion-block';
wrapper.contentEditable = 'true';
wrapper.innerHTML = eventNotesArea.innerHTML;
eventNotesArea.innerHTML = '';
eventNotesArea.appendChild(wrapper);
}
}
ensureDefaultBlock();
}
} else {
document.getElementById('eventModalSubtitle').textContent = 'New Event';
eventTitleInput.value = '';
eventNotesArea.innerHTML = '';
currentEventSketches = [];
if (deleteEventBtn) deleteEventBtn.classList.add('hidden');
ensureDefaultBlock();
setTimeout(() => eventTitleInput.focus(), 100);
}
}
renderSketchesGallery();
}
if (closeEventModal) {
closeEventModal.addEventListener('click', () => {
eventEditorModal.classList.add('hidden');
slashMenu.classList.add('hidden');
});
}
if (deleteEventBtn) {
deleteEventBtn.addEventListener('click', () => {
if (isStandaloneNote) {
if (currentEditingNoteId) {
standaloneNotes = standaloneNotes.filter(n => n.id !== currentEditingNoteId);
localStorage.setItem('fliqlow_standalone_notes', JSON.stringify(standaloneNotes));
renderNoteList();
closeEventModal.click();
}
} else {
if (currentEditingEventId) {
notionEvents = notionEvents.filter(e => e.id !== currentEditingEventId);
localStorage.setItem('notionEvents', JSON.stringify(notionEvents));
renderEvents();
closeEventModal.click();
}
}
});
}
if (saveEventBtn) {
saveEventBtn.addEventListener('click', () => {
if (isStandaloneNote) {
if (currentEditingNoteId) {
const note = standaloneNotes.find(n => n.id === currentEditingNoteId);
if (note) {
note.title = eventTitleInput.value;
note.content = eventNotesArea.innerHTML;
note.sketches = currentEventSketches;
note.updatedAt = Date.now();
}
} else {
const newNote = {
id: 'note_' + Date.now(),
title: eventTitleInput.value,
content: eventNotesArea.innerHTML,
sketches: currentEventSketches,
updatedAt: Date.now()
};
standaloneNotes.push(newNote);
}
localStorage.setItem('fliqlow_standalone_notes', JSON.stringify(standaloneNotes));
renderNoteList();
closeEventModal.click();
} else {
if (currentEditingEventId) {
const ev = notionEvents.find(e => e.id === currentEditingEventId);
if (ev) {
ev.title = eventTitleInput.value;
ev.notes = eventNotesArea.innerHTML;
ev.sketches = currentEventSketches;
}
} else {
const newEvent = {
id: Date.now().toString(),
dateStr: newEventDateStr,
startHour: newEventHour,
duration: 1, // default 1 hour block
title: eventTitleInput.value,
notes: eventNotesArea.innerHTML,
sketches: currentEventSketches
};
notionEvents.push(newEvent);
}
localStorage.setItem('notionEvents', JSON.stringify(notionEvents));
renderEvents();
closeEventModal.click();
}
});
}
// ==========================================
// SLASH COMMAND MENU & TRUE BLOCK EDITOR (PHASE 3)
// ==========================================
const slashMenu = document.getElementById('slashMenu');
const blockAddBtn = document.getElementById('blockAddBtn');
const blockDragBtn = document.getElementById('blockDragBtn');
let activeHoverNode = null;
let draggedBlock = null;
// Ensure event area always has one block
function ensureDefaultBlock() {
const textContent = eventNotesArea.textContent.trim();
if (eventNotesArea.children.length === 0 || textContent === '' || textContent === 'NaN') {
eventNotesArea.innerHTML = '';
}
}
eventNotesArea.addEventListener('focusout', () => {
setTimeout(ensureDefaultBlock, 100);
});
ensureDefaultBlock();
// Intercept Keypresses to Manage Blocks
eventNotesArea.addEventListener('keydown', (e) => {
const isBlock = e.target.classList.contains('notion-block');
if (!isBlock) return;
if (e.key === 'Enter' && !e.shiftKey) {
const todoItem = e.target.closest('.notion-todo-item');
if (todoItem) {
// We are inside a bullet or todo-list item
e.preventDefault();
slashMenu.classList.add('hidden');
if (e.target.textContent.trim() === '') {
// Empty item â exit list, create a plain block after the todo-item
const newBlock = document.createElement('div');
newBlock.className = 'notion-block';
newBlock.contentEditable = 'true';
todoItem.after(newBlock);
newBlock.focus();
} else {
// Non-empty item â duplicate the item type after it
const isTodo = todoItem.hasAttribute('data-checked');
const newItem = document.createElement('div');
newItem.contentEditable = 'false';
if (isTodo) {
newItem.className = 'notion-todo-item';
newItem.setAttribute('data-checked', 'false');
newItem.innerHTML = '';
} else {
newItem.className = 'notion-todo-item';
newItem.innerHTML = 'âĒ
';
}
todoItem.after(newItem);
const newText = newItem.querySelector('.todo-text');
if (newText) newText.focus();
}
} else {
// Normal paragraph block
e.preventDefault();
const newBlock = document.createElement('div');
newBlock.className = 'notion-block';
newBlock.contentEditable = 'true';
slashMenu.classList.add('hidden');
e.target.after(newBlock);
// Split text content if caret is in the middle
const sel = window.getSelection();
if (sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
if (range.endOffset < e.target.textContent.length) {
const txt = e.target.textContent;
newBlock.textContent = txt.slice(range.endOffset);
e.target.textContent = txt.slice(0, range.endOffset);
}
}
newBlock.focus();
}
} else if (e.key === 'Backspace') {
const todoItem = e.target.closest('.notion-todo-item');
if (e.target.textContent.length === 0) {
e.preventDefault();
if (todoItem) {
// Empty bullet/todo â remove the item, focus previous sibling
const prev = todoItem.previousElementSibling;
todoItem.remove();
if (prev) {
const focusTarget = prev.classList.contains('notion-todo-item')
? prev.querySelector('.notion-block')
: prev;
if (focusTarget) {
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(focusTarget);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
focusTarget.focus();
}
}
} else {
const prev = e.target.previousElementSibling;
if (prev && prev.classList.contains('notion-block')) {
e.target.remove();
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(prev);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
}
}
}
});
// Positioning Handles
eventNotesArea.addEventListener('mousemove', (e) => {
let node = document.elementFromPoint(e.clientX, e.clientY);
if (!node) return;
// Resolve block parent
if (!node.classList.contains('notion-block') && !node.classList.contains('notion-todo-item')) {
let parent = node.closest('.notion-block, .notion-todo-item');
if (parent) node = parent;
else return;
}
if (node && (node.classList.contains('notion-block') || node.classList.contains('notion-todo-item'))) {
activeHoverNode = node;
const rect = activeHoverNode.getBoundingClientRect();
const areaRect = eventNotesArea.getBoundingClientRect();
blockAddBtn.classList.remove('hidden');
blockDragBtn.classList.remove('hidden');
blockAddBtn.style.top = (rect.top - areaRect.top + 2) + 'px';
blockDragBtn.style.top = (rect.top - areaRect.top + 2) + 'px';
}
});
eventNotesArea.addEventListener('mouseleave', (e) => {
if (e.relatedTarget && e.relatedTarget.closest('.event-notes-wrapper')) return;
blockAddBtn.classList.add('hidden');
blockDragBtn.classList.add('hidden');
});
// Handle Slash Popup Location
blockAddBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (activeHoverNode) {
const btnRect = blockAddBtn.getBoundingClientRect();
slashMenu.style.left = (btnRect.right + 8) + 'px';
slashMenu.style.top = btnRect.top + 'px';
slashMenu.classList.remove('hidden');
}
});
function getCaretCoordinates() {
let x = 0, y = 0;
const isSupported = typeof window.getSelection !== "undefined";
if (isSupported) {
const selection = window.getSelection();
if (selection.rangeCount !== 0) {
const range = selection.getRangeAt(0).cloneRange();
range.collapse(true);
const rect = range.getClientRects()[0];
if (rect) {
x = rect.left;
y = rect.top;
}
}
}
return { x, y };
}
eventNotesArea.addEventListener('keyup', (e) => {
if (e.key === '/') {
const coords = getCaretCoordinates();
slashMenu.style.left = Math.max(30, coords.x - 20) + 'px';
slashMenu.style.top = (coords.y + 20) + 'px';
slashMenu.classList.remove('hidden');
} else if (e.key === 'Escape') {
slashMenu.classList.add('hidden');
}
});
document.addEventListener('click', (e) => {
if (!slashMenu.classList.contains('hidden') && !e.target.closest('#slashMenu') && !e.target.closest('#blockAddBtn')) {
slashMenu.classList.add('hidden');
}
});
const slashItems = document.querySelectorAll('.slash-item');
slashItems.forEach(item => {
item.addEventListener('click', () => {
const action = item.getAttribute('data-action');
slashMenu.classList.add('hidden');
// Inject format safely utilizing activeHoverNode
const targetNode = activeHoverNode || eventNotesArea.lastElementChild || eventNotesArea;
// Clear text if it's just formatting
targetNode.textContent = targetNode.textContent.replace('/', '');
if (action === 'todo') {
const todoHtml = `
`;
targetNode.outerHTML = todoHtml;
} else if (action === 'h2') {
targetNode.outerHTML = ``;
} else if (action === 'h3') {
targetNode.outerHTML = ``;
} else if (action === 'bullet') {
targetNode.outerHTML = `
`;
}
// Setup focus for new element
setTimeout(() => {
const created = eventNotesArea.querySelector('.notion-block[data-placeholder]');
if (created) created.focus();
}, 50);
});
});
// Drag and Drop Rearrangement (Node swapping)
blockDragBtn.setAttribute('draggable', 'true');
blockDragBtn.addEventListener('dragstart', (e) => {
if (activeHoverNode) {
draggedBlock = activeHoverNode;
setTimeout(() => draggedBlock.style.opacity = '0.5', 0);
// Set required data for Firefox
e.dataTransfer.setData('text/plain', '');
}
});
blockDragBtn.addEventListener('dragend', () => {
if (draggedBlock) draggedBlock.style.opacity = '1';
draggedBlock = null;
});
eventNotesArea.addEventListener('dragover', (e) => {
e.preventDefault();
if (!draggedBlock) return;
const target = e.target.closest('.notion-block, .notion-todo-item');
if (target && target !== draggedBlock && target.parentNode === eventNotesArea) {
const rect = target.getBoundingClientRect();
const after = e.clientY > rect.top + (rect.height / 2);
if (after) {
target.after(draggedBlock);
} else {
target.before(draggedBlock);
}
}
});
// Restore Checkbox Interactive Logic & Empty Space Focus
eventNotesArea.addEventListener('click', (e) => {
if (e.target === eventNotesArea) {
// If clicking inside the wrapper but outside blocks, focus last block
const lastChild = eventNotesArea.lastElementChild;
if (lastChild && (lastChild.classList.contains('notion-block') || lastChild.classList.contains('notion-todo-item'))) {
const blockToFocus = lastChild.classList.contains('notion-todo-item') ? lastChild.querySelector('.notion-block') : lastChild;
if (blockToFocus) {
const range = document.createRange();
range.selectNodeContents(blockToFocus);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
blockToFocus.focus();
}
}
return;
}
if (e.target.closest('.todo-checkbox-btn')) {
e.preventDefault();
const todoItem = e.target.closest('.notion-todo-item');
if (todoItem) {
const isChecked = todoItem.getAttribute('data-checked') === 'true';
todoItem.setAttribute('data-checked', isChecked ? 'false' : 'true');
const btn = todoItem.querySelector('.todo-checkbox-btn');
if (btn) btn.innerHTML = isChecked ? '' : '';
}
}
});
// ==========================================
// GOODNOTES-STYLE CANVAS SKETCHER
// ==========================================
const canvasOverlay = document.getElementById('canvasOverlay');
const addSketchPageBtn = document.getElementById('addSketchPageBtn');
const cancelCanvasBtn = document.getElementById('cancelCanvasBtn');
const saveCanvasBtn = document.getElementById('saveCanvasBtn');
const clearCanvasBtn = document.getElementById('clearCanvasBtn');
const sketchCanvas = document.getElementById('sketchCanvas');
const sketchCtx = sketchCanvas.getContext('2d');
const penToolBtn = document.getElementById('penToolBtn');
const eraserToolBtn = document.getElementById('eraserToolBtn');
const penColorPicker = document.getElementById('penColorPicker');
const penSizeSlider = document.getElementById('penSizeSlider');
let isDrawingCanvas = false;
let currentTool = 'pen'; // 'pen' | 'eraser'
function renderSketchesGallery() {
// Clear all except the "Add Blank Page" button
Array.from(sketchGallery.children).forEach(child => {
if (child.id !== 'addSketchPageBtn') {
child.remove();
}
});
currentEventSketches.forEach((sketchData, index) => {
const card = document.createElement('div');
card.className = 'sketch-card';
card.innerHTML = `
`;
sketchGallery.insertBefore(card, addSketchPageBtn);
card.querySelector('.sketch-card-delete').addEventListener('click', (e) => {
e.stopPropagation();
currentEventSketches.splice(index, 1);
renderSketchesGallery();
});
});
}
function resizeCanvas() {
// make it large for sketching
sketchCanvas.width = document.querySelector('.canvas-workspace').clientWidth - 40;
sketchCanvas.height = document.querySelector('.canvas-workspace').clientHeight - 40;
// Fill white or leave transparent
// Best is to leave transparent so it looks natural on dark mode, but let's give it an off-black background.
sketchCtx.fillStyle = '#111111';
sketchCtx.fillRect(0, 0, sketchCanvas.width, sketchCanvas.height);
}
if (addSketchPageBtn) {
addSketchPageBtn.addEventListener('click', () => {
canvasOverlay.classList.remove('hidden');
// Delay to allow DOM update for clientWidth
setTimeout(resizeCanvas, 50);
});
}
if (cancelCanvasBtn) {
cancelCanvasBtn.addEventListener('click', () => {
canvasOverlay.classList.add('hidden');
});
}
if (clearCanvasBtn) {
clearCanvasBtn.addEventListener('click', () => {
sketchCtx.fillStyle = '#111111';
sketchCtx.fillRect(0, 0, sketchCanvas.width, sketchCanvas.height);
});
}
if (saveCanvasBtn) {
saveCanvasBtn.addEventListener('click', () => {
const dataUrl = sketchCanvas.toDataURL('image/jpeg', 0.8);
currentEventSketches.push(dataUrl);
renderSketchesGallery();
canvasOverlay.classList.add('hidden');
});
}
// Tool switching
penToolBtn.addEventListener('click', () => { currentTool = 'pen'; penToolBtn.classList.add('active'); eraserToolBtn.classList.remove('active'); });
eraserToolBtn.addEventListener('click', () => { currentTool = 'eraser'; eraserToolBtn.classList.add('active'); penToolBtn.classList.remove('active'); });
// Drawing mechanics
function startPosition(e) {
isDrawingCanvas = true;
draw(e);
}
function finishPosition() {
isDrawingCanvas = false;
sketchCtx.beginPath();
}
function draw(e) {
if (!isDrawingCanvas) return;
// Support mouse & touch (simplified for mouse here)
const rect = sketchCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
sketchCtx.lineWidth = penSizeSlider.value;
sketchCtx.lineCap = 'round';
if (currentTool === 'eraser') {
sketchCtx.strokeStyle = '#111111'; // background color to 'erase'
} else {
sketchCtx.strokeStyle = penColorPicker.value;
}
sketchCtx.lineTo(x, y);
sketchCtx.stroke();
sketchCtx.beginPath();
sketchCtx.moveTo(x, y);
}
sketchCanvas.addEventListener('mousedown', startPosition);
sketchCanvas.addEventListener('mouseup', finishPosition);
sketchCanvas.addEventListener('mousemove', draw);
sketchCanvas.addEventListener('mouseleave', finishPosition);
// Pinch Zoom Logic for Calendar (iOS & Desktop)
let initialPinchDist = null;
let initialHourHeight = calHourHeight;
const calBodyEl = document.querySelector('.calendar-body');
if (calBodyEl) {
calBodyEl.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
// Prevent native pinch zoom
e.preventDefault();
calBodyEl.classList.add('pinching');
if (currentCalView !== 'month') {
initialPinchDist = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
initialHourHeight = calHourHeight;
}
}
}, { passive: false });
calBodyEl.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
if (currentCalView === 'month') {
// In month view, pinch can switch views
return;
}
if (initialPinchDist !== null) {
const currentDist = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
const scale = currentDist / initialPinchDist;
calHourHeight = Math.max(30, Math.min(150, initialHourHeight * scale));
// Update time label heights to match
const timeLabels = document.querySelectorAll('.cal-time-label');
timeLabels.forEach(label => {
label.style.height = calHourHeight + 'px';
});
// Update grid background lines
const calGridEl = document.getElementById('calGrid');
if (calGridEl) {
const lineH = calHourHeight - 1;
calGridEl.style.backgroundImage = `repeating-linear-gradient(to bottom, transparent, transparent ${lineH}px, rgba(255,255,255,0.05) ${lineH}px, rgba(255,255,255,0.05) ${calHourHeight}px)`;
}
renderEvents();
}
}
}, { passive: false });
calBodyEl.addEventListener('touchend', (e) => {
if (e.touches.length < 2) {
initialPinchDist = null;
calBodyEl.classList.remove('pinching');
}
});
// Gesturestart/gesturechange for Safari (additional iOS support)
calBodyEl.addEventListener('gesturestart', (e) => {
e.preventDefault();
});
calBodyEl.addEventListener('gesturechange', (e) => {
e.preventDefault();
});
calBodyEl.addEventListener('gestureend', (e) => {
e.preventDefault();
});
}
// ==========================================
// PREMIUM UPGRADE LOGIC
// ==========================================
// Ambience Audio toggle â handler registered below in AMBIENCE TOGGLE LOGIC section
const ambienceToggleBtnSidebar = document.getElementById('ambienceToggleBtn');
const ambienceDrawerSidebar = document.getElementById('ambienceDrawer');
// Task Panel toggle
const taskPanelToggleBtn = document.getElementById('taskPanelToggleBtn');
const taskPanelSidebar = document.getElementById('taskPanel');
const closeTaskPanelSidebar = document.getElementById('closeTaskPanel');
if (taskPanelToggleBtn && taskPanelSidebar) {
taskPanelToggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
taskPanelSidebar.classList.toggle('hidden');
taskPanelToggleBtn.classList.toggle('active', !taskPanelSidebar.classList.contains('hidden'));
});
}
if (closeTaskPanelSidebar && taskPanelSidebar) {
closeTaskPanelSidebar.addEventListener('click', () => {
taskPanelSidebar.classList.add('hidden');
if (taskPanelToggleBtn) taskPanelToggleBtn.classList.remove('active');
});
}
// Alert Audio Context setup
let alertAudioCtx = null;
const alertVolumeSlider = document.getElementById('alertVolumeSlider');
const alertToggleBtn = document.getElementById('alertToggleBtn');
const alertDrawer = document.getElementById('alertDrawer');
if (alertToggleBtn) {
alertToggleBtn.addEventListener('click', () => {
alertDrawer.classList.toggle('hidden');
alertToggleBtn.classList.toggle('active', !alertDrawer.classList.contains('hidden'));
});
}
document.addEventListener('click', (e) => {
if (alertDrawer && !alertDrawer.classList.contains('hidden') && !e.target.closest('#alertPanel')) {
alertDrawer.classList.add('hidden');
if (alertToggleBtn) alertToggleBtn.classList.remove('active');
}
if (ambienceDrawerSidebar && !ambienceDrawerSidebar.classList.contains('hidden') && !e.target.closest('#ambienceDrawer') && !e.target.closest('#ambienceToggleBtn')) {
ambienceDrawerSidebar.classList.add('hidden');
if (ambienceToggleBtnSidebar) ambienceToggleBtnSidebar.classList.remove('active');
}
});
window.playAlertSound = function () {
if (!alertAudioCtx) {
alertAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
let vol = alertVolumeSlider ? parseInt(alertVolumeSlider.value) / 100 : 0.5;
if (vol === 0) return;
// A pleasant soft double-chime
function playChime(freq, startTime, duration) {
const osc = alertAudioCtx.createOscillator();
const gainNode = alertAudioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, startTime);
gainNode.gain.setValueAtTime(0, startTime);
gainNode.gain.linearRampToValueAtTime(vol * 0.5, startTime + 0.05);
gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + duration);
osc.connect(gainNode);
gainNode.connect(alertAudioCtx.destination);
osc.start(startTime);
osc.stop(startTime + duration);
}
const t = alertAudioCtx.currentTime;
playChime(880, t, 1.5); // A5
playChime(1108.73, t + 0.15, 1.5); // C#6
};
// ==========================================
// FOCUS STATS TRACKING (REWRITE)
// ==========================================
function getLocalDateKey(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
function formatSeconds(totalSec) {
const hrs = Math.floor(totalSec / 3600);
const mins = Math.floor((totalSec % 3600) / 60);
if (hrs > 0) return hrs + 'h ' + mins + 'm';
return mins + 'm';
}
const DEFAULT_STATS = {
totalAppSeconds: 0,
totalFocusSeconds: 0,
totalStartClicks: 0,
completedFocusSessions: 0,
currentStreak: 0,
longestStreak: 0,
lastSessionDate: null,
daily: {}
};
let focusStats = (() => {
try {
const raw = JSON.parse(localStorage.getItem('fliqlow_stats_v2'));
if (raw && typeof raw.totalAppSeconds === 'number') return { ...DEFAULT_STATS, ...raw };
} catch (e) { }
// Migrate from old stats if present
try {
const old = JSON.parse(localStorage.getItem('fliqlow_stats'));
if (old) {
return {
...DEFAULT_STATS,
totalFocusSeconds: old.totalFocusSeconds || 0,
totalStartClicks: old.startClicks || 0,
completedFocusSessions: old.totalSessions || 0,
currentStreak: old.streak || 0,
longestStreak: old.streak || 0,
daily: {}
};
}
} catch (e) { }
return { ...DEFAULT_STATS };
})();
function saveStats() {
// Prune daily entries older than 30 days
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 30);
const cutoffKey = getLocalDateKey(cutoff);
Object.keys(focusStats.daily).forEach(k => {
if (k < cutoffKey) delete focusStats.daily[k];
});
localStorage.setItem('fliqlow_stats_v2', JSON.stringify(focusStats));
}
function ensureDailyEntry() {
const today = getLocalDateKey(new Date());
if (!focusStats.daily[today]) {
focusStats.daily[today] = { appSeconds: 0, focusSeconds: 0, startClicks: 0, completedFocusSessions: 0 };
}
return today;
}
// App time tracking (visibility-aware, 1s tick)
let _appTimeInterval = null;
function startAppTimeTracking() {
if (_appTimeInterval) return;
_appTimeInterval = setInterval(() => {
if (document.visibilityState === 'visible') {
focusStats.totalAppSeconds++;
const today = ensureDailyEntry();
focusStats.daily[today].appSeconds++;
// Also track focus time if running pomodoro
if (isRunning && currentMode === 'pomodoro') {
focusStats.totalFocusSeconds++;
focusStats.daily[today].focusSeconds++;
}
// Save every 30 seconds to avoid excessive writes
if (focusStats.totalAppSeconds % 30 === 0) saveStats();
}
}, 1000);
}
startAppTimeTracking();
// Save stats on page unload
window.addEventListener('beforeunload', saveStats);
// Track start clicks (called from toggleTimer)
function trackStartClick() {
focusStats.totalStartClicks++;
const today = ensureDailyEntry();
focusStats.daily[today].startClicks++;
saveStats();
}
// Completed session tracking
window.incrementSessionStat = function () {
focusStats.completedFocusSessions++;
const today = ensureDailyEntry();
focusStats.daily[today].completedFocusSessions++;
// Streak logic: each completed session increments streak
focusStats.currentStreak++;
if (focusStats.currentStreak > focusStats.longestStreak) {
focusStats.longestStreak = focusStats.currentStreak;
}
focusStats.lastSessionDate = today;
saveStats();
renderStats();
renderSessionTally();
updateStreakIndicator();
};
function updateStreakIndicator() {
const el = document.getElementById('streakCount');
const container = document.getElementById('streakIndicator');
if (el) el.textContent = focusStats.currentStreak;
if (container) {
container.classList.toggle('has-streak', focusStats.currentStreak > 0);
}
}
updateStreakIndicator();
const statsModal = document.getElementById('statsModal');
const statsToggleBtn = document.getElementById('statsToggleBtn');
const closeStats = document.getElementById('closeStats');
const mobileCloseStats = document.getElementById('mobileCloseStats');
if (statsToggleBtn) statsToggleBtn.addEventListener('click', () => {
renderStats();
statsModal.classList.remove('hidden');
});
if (closeStats) closeStats.addEventListener('click', () => statsModal.classList.add('hidden'));
if (mobileCloseStats) mobileCloseStats.addEventListener('click', () => statsModal.classList.add('hidden'));
function renderStats() {
const appTimeEl = document.getElementById('statTotalAppTime');
if (appTimeEl) appTimeEl.textContent = formatSeconds(focusStats.totalAppSeconds);
const focusTimeEl = document.getElementById('statTotalFocusTime');
if (focusTimeEl) focusTimeEl.textContent = formatSeconds(focusStats.totalFocusSeconds);
const clicksEl = document.getElementById('statStartClicks');
if (clicksEl) clicksEl.textContent = focusStats.totalStartClicks;
const sessionsEl = document.getElementById('statSessionsCompleted');
if (sessionsEl) sessionsEl.textContent = focusStats.completedFocusSessions;
const streakEl = document.getElementById('statCurrentStreak');
if (streakEl) streakEl.textContent = focusStats.currentStreak;
const longestEl = document.getElementById('statLongestStreak');
if (longestEl) longestEl.textContent = focusStats.longestStreak;
renderProductivityChart();
}
function renderProductivityChart() {
const canvas = document.getElementById('productivityGraph');
if (!canvas) return;
const ctx2 = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = 200 * dpr;
canvas.style.width = rect.width + 'px';
canvas.style.height = '200px';
ctx2.scale(dpr, dpr);
const W = rect.width;
const H = 200;
ctx2.clearRect(0, 0, W, H);
// Build last 7 days data from daily stats
const today = new Date();
today.setHours(0, 0, 0, 0);
const dailySeconds = [];
const dayLabels = [];
for (let i = 6; i >= 0; i--) {
const d = new Date(today);
d.setDate(d.getDate() - i);
const key = getLocalDateKey(d);
const entry = focusStats.daily[key];
dailySeconds.push(entry ? entry.focusSeconds : 0);
dayLabels.push(d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }));
}
let maxSec = Math.max(...dailySeconds);
if (maxSec === 0) maxSec = 3600; // Default 1h
// Smart Y-axis
const maxHrs = maxSec / 3600;
let yMax;
if (maxHrs <= 1) yMax = 1;
else if (maxHrs <= 2) yMax = 2;
else if (maxHrs <= 5) yMax = 5;
else if (maxHrs <= 10) yMax = 10;
else if (maxHrs <= 24) yMax = 24;
else yMax = Math.ceil(maxHrs / 5) * 5;
const yMaxSec = yMax * 3600;
const padding = { top: 15, right: 15, bottom: 30, left: 50 };
const chartW = W - padding.left - padding.right;
const chartH = H - padding.top - padding.bottom;
const barW = (chartW / 7) * 0.5;
const barGap = (chartW / 7);
// Y-axis
ctx2.fillStyle = '#666';
ctx2.font = '11px Inter, sans-serif';
ctx2.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const y = padding.top + chartH - (chartH * i / 4);
const val = (yMax * i / 4);
ctx2.fillText(val >= 1 ? val + 'h' : Math.round(val * 60) + 'm', padding.left - 8, y + 4);
ctx2.strokeStyle = 'rgba(255,255,255,0.05)';
ctx2.lineWidth = 1;
ctx2.beginPath();
ctx2.moveTo(padding.left, y);
ctx2.lineTo(W - padding.right, y);
ctx2.stroke();
}
// Bars
dailySeconds.forEach((sec, idx) => {
const barH = (sec / yMaxSec) * chartH;
const x = padding.left + (idx * barGap) + (barGap - barW) / 2;
const y = padding.top + chartH - barH;
const gradient = ctx2.createLinearGradient(x, y + barH, x, y);
gradient.addColorStop(0, 'rgba(99, 102, 241, 0.8)');
gradient.addColorStop(1, 'rgba(168, 85, 247, 0.9)');
ctx2.fillStyle = barH > 0 ? gradient : 'rgba(255,255,255,0.03)';
ctx2.beginPath();
ctx2.roundRect(x, barH > 0 ? y : padding.top + chartH - 2, barW, Math.max(barH, 2), [4, 4, 0, 0]);
ctx2.fill();
ctx2.fillStyle = '#666';
ctx2.font = '10px Inter, sans-serif';
ctx2.textAlign = 'center';
ctx2.fillText(dayLabels[idx], x + barW / 2, H - 8);
});
}
// Session Tally Emoji System
const tallyContainer = document.getElementById('sessionTallyContainer');
let currentTallyStyle = localStorage.getItem('fliqlow_tally_style') || 'tomatoes';
const customEmojiSetsRaw = JSON.parse(localStorage.getItem('fliqlow_custom_emojis')) || {};
const customEmojiSets = {};
Object.keys(customEmojiSetsRaw).forEach(k => {
if (Array.isArray(customEmojiSetsRaw[k])) {
customEmojiSets[k] = { name: 'Custom', emojis: customEmojiSetsRaw[k] };
} else {
customEmojiSets[k] = customEmojiSetsRaw[k];
}
});
const tallyDictionaries = {
tomatoes: ['ð
', 'ð
', 'ð
', 'ð
'],
plants: ['ðą', 'ðŋ', 'ðŠī', 'ðē'],
dots: ['âŠ', 'âŠ', 'âŠ', 'âŠ'],
hearts: ['ð', 'ð', 'ð', 'ð'],
stars: ['â', 'ð', 'âĻ', 'ðŦ'],
space: ['ð', 'ðļ', 'ð°ïļ', 'ðŠ'],
fire: ['ðĨ', 'ðĨ', 'ðĨ', 'ðĨ'],
brain: ['ð§ ', 'ð§ ', 'ð§ ', 'ð§ '],
hamster: ['ðđ', 'ðđ', 'ðđ', 'ðđ'],
book: ['ð', 'ð', 'ð', 'ð'],
coffee: ['â', 'â', 'â', 'â'],
wave: ['ð', 'ð', 'ð', 'ð']
};
Object.keys(customEmojiSets).forEach(k => {
tallyDictionaries[k] = customEmojiSets[k].emojis;
});
const emojiPickerToggle = document.getElementById('emojiPickerToggle');
const emojiPickerPopup = document.getElementById('emojiPickerPopup');
const emojiOpts = document.querySelectorAll('.emoji-opt');
if (emojiPickerToggle && emojiPickerPopup) {
emojiPickerToggle.addEventListener('click', () => {
emojiPickerPopup.classList.toggle('hidden');
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!e.target.closest('#sessionTallyContainer') && !e.target.closest('#emojiPickerPopup')) {
emojiPickerPopup.classList.add('hidden');
}
});
}
function updateTallyStyle(style, customEmojis = null) {
currentTallyStyle = style;
localStorage.setItem('fliqlow_tally_style', currentTallyStyle);
if (customEmojis) {
tallyDictionaries[style] = Array.isArray(customEmojis) ? customEmojis : Array.from(new Intl.Segmenter(navigator.language, { granularity: 'grapheme' }).segment(customEmojis)).map(x => x.segment);
}
// Update active class on static options
document.querySelectorAll('.emoji-opt').forEach(o => o.classList.toggle('active', o.dataset.style === currentTallyStyle));
// Update active class on custom sets
document.querySelectorAll('.custom-tally-item').forEach(o => {
if (o.dataset.style === currentTallyStyle) {
o.style.borderColor = '#a855f7';
o.style.background = 'rgba(168, 85, 247, 0.2)';
} else {
o.style.borderColor = 'rgba(255,255,255,0.1)';
o.style.background = 'rgba(255,255,255,0.05)';
}
});
if (emojiPickerPopup) emojiPickerPopup.classList.add('hidden');
renderSessionTally();
}
document.querySelectorAll('.emoji-opt').forEach(opt => {
opt.addEventListener('click', () => {
updateTallyStyle(opt.dataset.style);
});
});
const saveCustomEmojiBtn = document.getElementById('saveCustomEmojiBtn');
const customEmojiInput = document.getElementById('customEmojiInput');
const customEmojiNameInput = document.getElementById('customEmojiNameInput');
const customTallySetsContainer = document.getElementById('customTallySetsContainer');
function renderCustomTallySets() {
if (!customTallySetsContainer) return;
customTallySetsContainer.innerHTML = '';
getAllTallyEmojiSets().then(sets => {
sets.sort((a, b) => b.createdAt - a.createdAt);
sets.forEach(set => {
const el = document.createElement('div');
el.className = 'custom-tally-set-item';
el.style.display = 'flex';
el.style.justifyContent = 'space-between';
el.style.alignItems = 'center';
el.style.padding = '8px';
el.style.marginBottom = '6px';
el.style.borderRadius = '6px';
el.style.border = '1px solid rgba(255,255,255,0.1)';
el.style.background = 'rgba(255,255,255,0.05)';
if (currentTallyStyle === set.id) {
el.style.borderColor = '#a855f7';
el.style.background = 'rgba(168, 85, 247, 0.2)';
}
el.innerHTML = `
${set.name}
${set.emojis}
`;
el.querySelector('.tally-set-info').addEventListener('click', () => {
updateTallyStyle(set.id, set.emojis);
});
el.querySelector('.delete-tally-btn').addEventListener('click', (e) => {
e.stopPropagation();
deleteTallyEmojiSet(set.id).then(renderCustomTallySets);
});
customTallySetsContainer.appendChild(el);
});
});
}
if (saveCustomEmojiBtn) {
saveCustomEmojiBtn.addEventListener('click', () => {
if (!customEmojiInput) return;
const emojis = customEmojiInput.value.trim();
const name = (customEmojiNameInput && customEmojiNameInput.value.trim()) || 'Custom Set';
if (!emojis) return;
const setObj = {
id: 'tally_' + Date.now(),
name: name,
emojis: emojis,
createdAt: Date.now()
};
saveTallyEmojiSet(setObj).then(() => {
customEmojiInput.value = '';
if (customEmojiNameInput) customEmojiNameInput.value = '';
renderCustomTallySets();
updateTallyStyle(setObj.id, setObj.emojis);
});
});
}
function initCustomEmojisUI() {
renderCustomTallySets();
}
initCustomEmojisUI();
(function initTallyStyle() {
emojiOpts.forEach(o => o.classList.toggle('active', o.dataset.style === currentTallyStyle));
renderSessionTally();
})();
function renderSessionTally() {
if (!tallyContainer) return;
const count = focusStats.completedFocusSessions || 0;
tallyContainer.style.display = 'flex';
// Check if we need to insert the wrapper inside the container dynamically
let wrapper = document.getElementById('tallyEmojiWrapper');
if (!wrapper) {
// Find badge and toggle
const badge = document.getElementById('sessionCounterBadge');
const toggle = document.getElementById('emojiPickerToggle');
tallyContainer.innerHTML = '';
wrapper = document.createElement('div');
wrapper.id = 'tallyEmojiWrapper';
wrapper.style.display = 'flex';
wrapper.style.gap = '15px';
wrapper.style.alignItems = 'center';
tallyContainer.appendChild(wrapper);
if (badge) tallyContainer.appendChild(badge);
if (toggle) tallyContainer.appendChild(toggle);
}
const badge = document.getElementById('sessionCounterBadge');
if (badge) badge.textContent = count;
wrapper.innerHTML = '';
const dict = tallyDictionaries[currentTallyStyle] || tallyDictionaries['tomatoes'];
// Use the set's emoji count as max slots
const MAX_SLOTS = dict.length;
const cycleProgress = count % MAX_SLOTS;
// How many to show as active: if count > 0 and cycleProgress === 0, show all as briefly completed then reset
const activeCount = (count > 0 && cycleProgress === 0) ? 0 : cycleProgress;
for (let i = 0; i < MAX_SLOTS; i++) {
const span = document.createElement('span');
span.className = 'tally-icon';
const emojiToUse = dict[i % dict.length];
span.textContent = emojiToUse;
if (i < activeCount) {
span.classList.add('active');
span.style.opacity = '1';
span.style.filter = 'drop-shadow(0 0 6px rgba(255, 255, 255, 0.4))';
} else {
span.classList.add('faded');
span.style.opacity = '0.2';
}
wrapper.appendChild(span);
}
}
// ==========================================
// YOUTUBE PIP WIDGET
// ==========================================
const youtubeWidget = document.getElementById('youtubeWidget');
const youtubeToggle = document.getElementById('youtubeToggle');
const ytCloseBtn = document.getElementById('ytCloseBtn');
const ytMinimizeBtn = document.getElementById('ytMinimizeBtn');
const loadYoutubeBtn = document.getElementById('loadYoutubeBtn');
const youtubeLink = document.getElementById('youtubeLink');
const ytDragHandle = document.getElementById('ytDragHandle');
if (youtubeToggle && youtubeWidget) {
youtubeToggle.addEventListener('click', () => {
youtubeWidget.classList.toggle('hidden');
});
}
if (ytCloseBtn && youtubeWidget) {
ytCloseBtn.addEventListener('click', () => {
youtubeWidget.classList.add('hidden');
youtubeWidget.classList.remove('minimized');
});
}
if (ytMinimizeBtn && youtubeWidget) {
ytMinimizeBtn.addEventListener('click', () => {
youtubeWidget.classList.toggle('minimized');
const icon = ytMinimizeBtn.querySelector('i');
if (youtubeWidget.classList.contains('minimized')) {
icon.className = 'fas fa-expand';
} else {
icon.className = 'fas fa-minus';
}
});
}
function parseYoutubeUrl(url) {
if (!url) return null;
// Support various YouTube URL formats
let videoId = null;
// youtu.be/ID
const shortMatch = url.match(/youtu\.be\/([a-zA-Z0-9_-]+)/);
if (shortMatch) videoId = shortMatch[1];
// youtube.com/watch?v=ID
const watchMatch = url.match(/[?&]v=([a-zA-Z0-9_-]+)/);
if (watchMatch) videoId = watchMatch[1];
// youtube.com/embed/ID
const embedMatch = url.match(/youtube\.com\/embed\/([a-zA-Z0-9_-]+)/);
if (embedMatch) videoId = embedMatch[1];
// youtube.com/shorts/ID
const shortsMatch = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]+)/);
if (shortsMatch) videoId = shortsMatch[1];
if (videoId) {
return `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`;
}
return null;
}
const ytFullscreenBtn = document.getElementById('ytFullscreenBtn');
const ytChangeLinkDiv = document.getElementById('ytChangeLinkDiv');
const ytInputGroup = document.getElementById('ytInputGroup');
const ytChangeLinkBtn = document.getElementById('ytChangeLinkBtn');
if (ytFullscreenBtn) {
ytFullscreenBtn.addEventListener('click', () => {
youtubeWidget.classList.toggle('fullscreen-mode');
const icon = ytFullscreenBtn.querySelector('i');
if (youtubeWidget.classList.contains('fullscreen-mode')) {
icon.className = 'fas fa-compress';
} else {
icon.className = 'fas fa-expand';
}
});
}
if (ytChangeLinkBtn) {
ytChangeLinkBtn.addEventListener('click', () => {
ytInputGroup.classList.remove('hidden');
ytChangeLinkDiv.classList.add('hidden');
});
}
if (loadYoutubeBtn) {
loadYoutubeBtn.addEventListener('click', () => {
const rawUrl = youtubeLink.value.trim();
if (!rawUrl) return;
const embedUrl = parseYoutubeUrl(rawUrl);
if (embedUrl) {
const playerContainer = document.getElementById('youtubePlayerContainer');
playerContainer.innerHTML = ``;
if (ytInputGroup && ytChangeLinkDiv) {
ytInputGroup.classList.add('hidden');
ytChangeLinkDiv.classList.remove('hidden');
}
}
});
}
// YouTube PiP Drag
if (ytDragHandle && youtubeWidget) {
let ytDragging = false;
let ytDragStartX = 0;
let ytDragStartY = 0;
let ytStartLeft = 20;
let ytStartBottom = 20;
ytDragHandle.addEventListener('mousedown', (e) => {
if (e.target.closest('.yt-pip-btn')) return;
ytDragging = true;
ytDragStartX = e.clientX;
ytDragStartY = e.clientY;
const rect = youtubeWidget.getBoundingClientRect();
ytStartLeft = rect.left;
ytStartBottom = window.innerHeight - rect.bottom;
youtubeWidget.style.transition = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!ytDragging) return;
const deltaX = e.clientX - ytDragStartX;
const deltaY = e.clientY - ytDragStartY;
let newLeft = ytStartLeft + deltaX;
let newBottom = ytStartBottom - deltaY;
// Clamp
newLeft = Math.max(0, Math.min(window.innerWidth - 200, newLeft));
newBottom = Math.max(0, Math.min(window.innerHeight - 100, newBottom));
youtubeWidget.style.left = newLeft + 'px';
youtubeWidget.style.bottom = newBottom + 'px';
});
document.addEventListener('mouseup', () => {
if (ytDragging) {
ytDragging = false;
youtubeWidget.style.transition = '';
}
});
// Touch drag support
ytDragHandle.addEventListener('touchstart', (e) => {
if (e.target.closest('.yt-pip-btn') || e.touches.length !== 1) return;
ytDragging = true;
ytDragStartX = e.touches[0].clientX;
ytDragStartY = e.touches[0].clientY;
const rect = youtubeWidget.getBoundingClientRect();
ytStartLeft = rect.left;
ytStartBottom = window.innerHeight - rect.bottom;
youtubeWidget.style.transition = 'none';
}, { passive: true });
document.addEventListener('touchmove', (e) => {
if (!ytDragging || e.touches.length !== 1) return;
const deltaX = e.touches[0].clientX - ytDragStartX;
const deltaY = e.touches[0].clientY - ytDragStartY;
let newLeft = ytStartLeft + deltaX;
let newBottom = ytStartBottom - deltaY;
newLeft = Math.max(0, Math.min(window.innerWidth - 200, newLeft));
newBottom = Math.max(0, Math.min(window.innerHeight - 100, newBottom));
youtubeWidget.style.left = newLeft + 'px';
youtubeWidget.style.bottom = newBottom + 'px';
}, { passive: true });
document.addEventListener('touchend', () => {
if (ytDragging) {
ytDragging = false;
youtubeWidget.style.transition = '';
}
});
}
// ==========================================
// PET TRACKER RESIZE HANDLE
// ==========================================
const petResizeHandle = document.getElementById('petResizeHandle');
const taskTrackerWrapper = document.querySelector('.task-tracker-wrapper');
if (petResizeHandle && taskTrackerWrapper) {
let isResizing = false;
let resizeStartX = 0;
let resizeStartY = 0;
let resizeStartScale = 1;
function getCurrentScale() {
const val = getComputedStyle(document.documentElement).getPropertyValue('--tracker-scale');
return parseFloat(val) || 1;
}
}
// ==========================================
// PET TRACKER RESIZE HANDLE
// ==========================================
if (petResizeHandle && taskTrackerWrapper) {
let isResizing = false;
let resizeStartX = 0;
let resizeStartY = 0;
let resizeStartScale = 1;
function getCurrentScale() {
const val = getComputedStyle(document.documentElement).getPropertyValue('--tracker-scale');
return parseFloat(val) || 1;
}
petResizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
resizeStartX = e.clientX;
resizeStartY = e.clientY;
resizeStartScale = getCurrentScale();
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
// Diagonal drag: bottom-left is resize direction
const deltaX = resizeStartX - e.clientX;
const deltaY = e.clientY - resizeStartY;
const delta = (deltaX + deltaY) / 200;
let newScale = resizeStartScale + delta;
newScale = Math.max(0.5, Math.min(1.5, newScale));
document.documentElement.style.setProperty('--tracker-scale', newScale);
});
document.addEventListener('mouseup', () => {
isResizing = false;
});
// Touch support
petResizeHandle.addEventListener('touchstart', (e) => {
if (e.touches.length !== 1) return;
isResizing = true;
resizeStartX = e.touches[0].clientX;
resizeStartY = e.touches[0].clientY;
resizeStartScale = getCurrentScale();
e.stopPropagation();
}, { passive: true });
document.addEventListener('touchmove', (e) => {
if (!isResizing || e.touches.length !== 1) return;
const deltaX = resizeStartX - e.touches[0].clientX;
const deltaY = e.touches[0].clientY - resizeStartY;
const delta = (deltaX + deltaY) / 200;
let newScale = resizeStartScale + delta;
newScale = Math.max(0.5, Math.min(1.5, newScale));
document.documentElement.style.setProperty('--tracker-scale', newScale);
}, { passive: true });
document.addEventListener('touchend', () => {
isResizing = false;
});
// Double-click to toggle expand/collapse
taskTrackerWrapper.addEventListener('dblclick', (e) => {
if (e.target.closest('button') || e.target.closest('input')) return;
const currentScale = getCurrentScale();
const newScale = currentScale < 1 ? 1 : 0.6;
document.documentElement.style.setProperty('--tracker-scale', newScale);
});
} // end if (petResizeHandle && taskTrackerWrapper)
// ==========================================
// GLOBAL ESCAPE KEY HANDLER
// ==========================================
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
// Close stats
const sm = document.getElementById('statsModal');
if (sm && !sm.classList.contains('hidden')) { sm.classList.add('hidden'); return; }
// Close eventEditorModal (Notes)
const eem = document.getElementById('eventEditorModal');
if (eem && !eem.classList.contains('hidden')) { eem.classList.add('hidden'); return; }
// Close calendar
const calO = document.getElementById('calendarOverlay');
if (calO && !calO.classList.contains('hidden')) { calO.classList.add('hidden'); return; }
// Close taskPanel
const taskP = document.getElementById('taskPanel');
if (taskP && !taskP.classList.contains('hidden')) { taskP.classList.add('hidden'); return; }
// Close settings
const setM = document.getElementById('settingsModal');
if (setM && !setM.classList.contains('hidden')) { setM.classList.add('hidden'); return; }
// Close scene panel
const sp = document.getElementById('scenePanel');
if (sp && !sp.classList.contains('hidden')) { sp.classList.add('hidden'); return; }
// Close alert drawer
const ad = document.getElementById('alertDrawer');
if (ad && !ad.classList.contains('hidden')) { ad.classList.add('hidden'); return; }
// Close YouTube fullscreen
const yw = document.getElementById('youtubeWidget');
if (yw && yw.classList.contains('fullscreen-mode')) { yw.classList.remove('fullscreen-mode'); return; }
}
});
// Close alert drawer on outside click
document.addEventListener('click', (e) => {
const alertDrawer = document.getElementById('alertDrawer');
const alertPanel = document.getElementById('alertPanel');
if (alertDrawer && !alertDrawer.classList.contains('hidden')) {
if (!e.target.closest('#alertPanel')) {
alertDrawer.classList.add('hidden');
}
}
});
// Close stats modal on outside click (click on the modal backdrop)
document.addEventListener('click', (e) => {
const sm = document.getElementById('statsModal');
if (sm && !sm.classList.contains('hidden')) {
if (e.target === sm) {
sm.classList.add('hidden');
}
}
});
// ==========================================
// PAGE NAVIGATION SYSTEM
// ==========================================
const pages = document.querySelectorAll('.app-page');
const navItems = document.querySelectorAll('.nav-item');
const startFocusBtn = document.getElementById('startFocusBtn');
function switchPage(pageId) {
pages.forEach(page => {
if (page.id === pageId + 'Page') {
page.classList.remove('hidden');
} else {
page.classList.add('hidden');
}
});
navItems.forEach(item => {
if (item.getAttribute('data-page') === pageId) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
// Logo visibility or other global adjustments
const logo = document.getElementById('mainLogo');
if (logo) {
if (pageId === 'focus') {
logo.style.opacity = '0.5';
} else {
logo.style.opacity = '1';
}
}
localStorage.setItem('fliqlow_last_page', pageId);
}
if (navItems) {
navItems.forEach(item => {
item.addEventListener('click', () => {
switchPage(item.getAttribute('data-page'));
});
});
}
if (startFocusBtn) {
startFocusBtn.addEventListener('click', () => switchPage('focus'));
}
// Restore last page
const lastPage = localStorage.getItem('fliqlow_last_page') || 'home';
switchPage(lastPage);
// ==========================================
// REALTIME FLIP CLOCK LOGIC
// ==========================================
function createFlipClockHTML(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = `
`;
}
function updateRealtimeClock(containerId) {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
updateRealtimeSection(containerId + '_hours', hours);
updateRealtimeSection(containerId + '_minutes', minutes);
}
function updateRealtimeSection(sectionId, value) {
const section = document.getElementById(sectionId);
if (!section) return;
const str = value.toString().padStart(2, '0');
const segments = section.querySelectorAll('.time-segment');
for (let i = 0; i < str.length; i++) {
const val = parseInt(str[i]);
const segment = segments[i];
const top = segment.querySelector('.segment-display__top');
const bottom = segment.querySelector('.segment-display__bottom');
const overTop = segment.querySelector('.segment-overlay__top');
const overBottom = segment.querySelector('.segment-overlay__bottom');
const overlay = segment.querySelector('.segment-overlay');
if (top.textContent !== str[i]) {
overlay.classList.add('flip');
top.textContent = str[i];
overBottom.textContent = str[i];
const finish = () => {
overlay.classList.remove('flip');
bottom.textContent = str[i];
overTop.textContent = str[i];
overlay.removeEventListener('animationend', finish);
};
overlay.addEventListener('animationend', finish);
}
}
}
// Init realtime clocks
createFlipClockHTML('realtimeClockHome');
createFlipClockHTML('realtimeClockFocus');
setInterval(() => {
updateRealtimeClock('realtimeClockHome');
updateRealtimeClock('realtimeClockFocus');
}, 1000);
// ==========================================
// MORE MENU LOGIC
// ==========================================
const moreMenuToggle = document.getElementById('moreMenuToggle');
const moreMenuDrawer = document.getElementById('moreMenuDrawer');
const realtimeClockToggle = document.getElementById('realtimeClockToggle');
const focusRealtimeClock = document.getElementById('focusRealtimeClock');
if (moreMenuToggle) {
moreMenuToggle.addEventListener('click', (e) => {
e.stopPropagation();
if (moreMenuDrawer) moreMenuDrawer.classList.toggle('hidden');
});
}
document.addEventListener('click', (e) => {
if (moreMenuDrawer && !moreMenuDrawer.classList.contains('hidden') && !e.target.closest('#moreMenuToggle')) {
moreMenuDrawer.classList.add('hidden');
}
});
if (realtimeClockToggle) {
realtimeClockToggle.addEventListener('click', () => {
if (focusRealtimeClock) {
focusRealtimeClock.classList.toggle('hidden');
localStorage.setItem('fliqlow_realtime_clock_visible', !focusRealtimeClock.classList.contains('hidden'));
}
});
}
// Restore realtime clock visibility
if (localStorage.getItem('fliqlow_realtime_clock_visible') === 'true') {
if (focusRealtimeClock) focusRealtimeClock.classList.remove('hidden');
}
// Close button on the floating realtime clock
const focusClockClose = document.getElementById('focusClockClose');
if (focusClockClose && focusRealtimeClock) {
focusClockClose.addEventListener('click', (e) => {
e.stopPropagation();
focusRealtimeClock.classList.add('hidden');
localStorage.setItem('fliqlow_realtime_clock_visible', 'false');
});
}
// ==========================================
// HOME PAGE MOTIVATION
// ==========================================
const HOME_QUOTES = [
"\"Time to recharge and reset.\"",
"\"I hope the world is kind to you.\"",
"\"One step at a time.\"",
"\"You are allowed to begin again.\"",
"\"Focus gently.\"",
"\"Deep work. Soft life.\""
];
const homeQuoteEl = document.getElementById('homeQuote');
if (homeQuoteEl) {
let quoteIdx = 0;
setInterval(() => {
quoteIdx = (quoteIdx + 1) % HOME_QUOTES.length;
homeQuoteEl.style.opacity = '0';
setTimeout(() => {
homeQuoteEl.textContent = HOME_QUOTES[quoteIdx];
homeQuoteEl.style.opacity = '1';
}, 1000);
}, 15000);
}
// ==========================================
// TALLY EMOJI SAVING SYSTEM
// ==========================================
const TALLY_STORE = 'saved_tally_emojis';
function initTallyDB() {
return new Promise((resolve, reject) => {
const DB_NAME = 'fliqlow_db';
const DB_VERSION = 2;
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = e => {
const db = e.target.result;
if (!db.objectStoreNames.contains(TALLY_STORE)) {
db.createObjectStore(TALLY_STORE, { keyPath: 'id' });
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
function saveTallyEmojiSet(setObj) {
return initTallyDB().then(db => {
return new Promise((resolve, reject) => {
const tx = db.transaction(TALLY_STORE, 'readwrite');
tx.objectStore(TALLY_STORE).put(setObj);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
});
}
function getAllTallyEmojiSets() {
return initTallyDB().then(db => {
const tx = db.transaction(TALLY_STORE, 'readonly');
const req = tx.objectStore(TALLY_STORE).getAll();
return new Promise(resolve => {
req.onsuccess = () => resolve(req.result || []);
});
});
}
function deleteTallyEmojiSet(id) {
return initTallyDB().then(db => {
return new Promise((resolve, reject) => {
const tx = db.transaction(TALLY_STORE, 'readwrite');
tx.objectStore(TALLY_STORE).delete(id);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
});
}
// Initial render
renderCustomTallySets();
// ==========================================
// LOCAL TIME WIDGET LOGIC
// ==========================================
const localTimeWidget = document.getElementById('localTimeWidget');
const localTimeToggle = document.getElementById('localTimeToggle');
const closeLocalTime = document.getElementById('closeLocalTime');
const collapseLocalTime = document.getElementById('collapseLocalTime');
const localTimeDisplay = document.getElementById('localTimeDisplay');
const localDateDisplay = document.getElementById('localDateDisplay');
function updateLocalTime() {
const now = new Date();
const timeStr = now.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
const dateStr = now.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' });
if (localTimeDisplay) localTimeDisplay.textContent = timeStr;
if (localDateDisplay) localDateDisplay.textContent = dateStr;
}
if (localTimeToggle) {
localTimeToggle.addEventListener('click', () => {
if (localTimeWidget) {
localTimeWidget.classList.toggle('hidden');
if (!localTimeWidget.classList.contains('hidden')) {
updateLocalTime();
}
}
});
}
if (closeLocalTime) {
closeLocalTime.addEventListener('click', () => {
if (localTimeWidget) localTimeWidget.classList.add('hidden');
});
}
if (collapseLocalTime) {
collapseLocalTime.addEventListener('click', () => {
if (localTimeWidget) {
localTimeWidget.classList.toggle('collapsed');
const icon = collapseLocalTime.querySelector('i');
if (localTimeWidget.classList.contains('collapsed')) {
icon.className = 'fas fa-chevron-up';
} else {
icon.className = 'fas fa-chevron-down';
}
}
});
}
setInterval(updateLocalTime, 1000);
updateLocalTime();
// Generic Draggable Logic for Floating Widgets
function makeDraggable(el, header) {
if (!el || !header) return;
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
header.onmousedown = dragMouseDown;
header.ontouchstart = dragTouchStart;
function dragMouseDown(e) {
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function dragTouchStart(e) {
if (e.touches.length !== 1) return;
pos3 = e.touches[0].clientX;
pos4 = e.touches[0].clientY;
document.ontouchend = closeDragElement;
document.ontouchmove = elementTouchDrag;
}
function elementDrag(e) {
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
el.style.top = (el.offsetTop - pos2) + "px";
el.style.left = (el.offsetLeft - pos1) + "px";
}
function elementTouchDrag(e) {
if (e.touches.length !== 1) return;
pos1 = pos3 - e.touches[0].clientX;
pos2 = pos4 - e.touches[0].clientY;
pos3 = e.touches[0].clientX;
pos4 = e.touches[0].clientY;
el.style.top = (el.offsetTop - pos2) + "px";
el.style.left = (el.offsetLeft - pos1) + "px";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
document.ontouchend = null;
document.ontouchmove = null;
}
}
if (localTimeWidget) {
const header = localTimeWidget.querySelector('.draggable-header');
if (header) makeDraggable(localTimeWidget, header);
}
// ==========================================
// AMBIENCE TOGGLE LOGIC
// ==========================================
const ambienceToggleBtn = document.getElementById('ambienceToggleBtn');
const ambienceDrawer = document.getElementById('ambienceDrawer');
if (ambienceToggleBtn && ambienceDrawer) {
ambienceToggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
ambienceDrawer.classList.toggle('hidden');
ambienceToggleBtn.classList.toggle('active', !ambienceDrawer.classList.contains('hidden'));
});
document.addEventListener('click', (e) => {
if (!ambienceDrawer.classList.contains('hidden') && !e.target.closest('#ambienceDrawer') && !e.target.closest('#ambienceToggleBtn')) {
ambienceDrawer.classList.add('hidden');
ambienceToggleBtn.classList.remove('active');
}
});
}
// ==========================================
// FOCUS WORLD COMPANION SYSTEM
// ==========================================
const _COMPANION_FPS = 10;
const _COMPANION_HEIGHT_PX = 78;
// prefix: filename prefix inside the folder (empty string = just the number)
// e.g. folder='knightmovement/', prefix='knightwalk', start=49 â 'knightmovement/knightwalk0049.png'
const _COMPANIONS = [
{ id: 'cat', name: 'Lily', bg: 'catbge.png', folder: 'catwalk/', prefix: '', start: 46, end: 63 },
{ id: 'girl', name: 'FeiFei', bg: 'girlbge.png', folder: 'girlwalking/', prefix: 'girlwalking', start: 1, end: 45 },
{ id: 'hamster', name: 'Bigbike', bg: 'hamsterbge.png', folder: 'hamsterwalk/', prefix: 'hamsterwalk', start: 48, end: 73 },
{ id: 'knight', name: 'Phu', bg: 'knightbge.png', folder: 'knightmovement/', prefix: 'knightwalk', start: 49, end: 78 },
{ id: 'traveler', name: 'John', bg: 'travelerbge.png', folder: 'travelermovement/', prefix: 'travelerwalk', start: 48, end: 78 },
{ id: 'turtle', name: 'Heng&Dee', bg: 'turtlebge.png', folder: 'turtlewalking/', prefix: 'turtlewalking',start: 3, end: 47 },
{ id: 'dog', name: 'Poppy', bg: 'dogbge.png', folder: 'dogwalk/', prefix: 'dogwalk', start: 1, end: 121 },
];
let _companionFrames = [];
let _companionFrameIdx = 0;
let _companionIntervalId = null;
let _companionPosX = 0;
let _companionDir = 1;
const _companionSpeed = 1.2;
let _activeCompanionIdx = (function() {
const v = parseInt(localStorage.getItem('fliqlow_companion_idx'), 10);
return (isNaN(v) || v < 0 || v >= _COMPANIONS.length) ? 1 : v; // 1 = FeiFei default
})();
function _saveCompanionIdx() {
localStorage.setItem('fliqlow_companion_idx', String(_activeCompanionIdx));
}
// Per-companion frame cache â loaded once, reused instantly on every switch
const _companionCache = {};
// Low-level: load an array of image URLs in parallel, return the ones that succeed
function _loadImages(urls) {
return Promise.all(urls.map(function(src) {
return new Promise(function(resolve) {
var img = new Image();
img.onload = function() { resolve(src); };
img.onerror = function() { resolve(null); };
img.src = src;
});
})).then(function(r) { return r.filter(Boolean); });
}
// Build ordered URL list for a companion
function _buildCompanionUrls(companion) {
var urls = [];
for (var i = companion.start; i <= companion.end; i++) {
urls.push(companion.folder + (companion.prefix || '') + String(i).padStart(4, '0') + '.png');
}
return urls;
}
// Full load (used by background preloader) â always resolves complete set
async function _loadCompanionFrames(companion) {
if (_companionCache[companion.id] && _companionCache[companion.id].length > 0) {
return _companionCache[companion.id];
}
const frames = await _loadImages(_buildCompanionUrls(companion));
_companionCache[companion.id] = frames;
return frames;
}
// Progressive switch: show companion after first QUICK frames, grow array in background.
// The animation loop uses `idx % frames.length` so extra frames slot in seamlessly.
const _QUICK_FRAMES = 6;
async function _switchCompanionProgressive(companion) {
// Already fully cached â instant
if (_companionCache[companion.id] && _companionCache[companion.id].length > 0) {
_companionFrames = _companionCache[companion.id];
return;
}
const urls = _buildCompanionUrls(companion);
const quickUrls = urls.slice(0, _QUICK_FRAMES);
const restUrls = urls.slice(_QUICK_FRAMES);
// Wait only for the quick batch (FeiFei's first frames are preloaded â near-instant)
const quickFrames = await _loadImages(quickUrls);
if (quickFrames.length === 0) {
// Fallback: full load if quick batch failed
const all = await _loadImages(urls);
_companionCache[companion.id] = all;
_companionFrames = all;
return;
}
// Show now with quick frames
_companionFrames = quickFrames; // live array â will grow below
// Load rest in background and push into the SAME array the animation loop is using
if (restUrls.length > 0) {
var liveArray = _companionFrames;
_loadImages(restUrls).then(function(rest) {
rest.forEach(function(src) { liveArray.push(src); });
// Only cache when we're still on this companion
if (_companionFrames === liveArray) {
_companionCache[companion.id] = liveArray.slice();
}
});
} else {
_companionCache[companion.id] = quickFrames.slice();
}
}
// Kick off bg-image preload immediately (tiny files, makes picker look instant)
function _preloadBgImages() {
_COMPANIONS.forEach(function(c) {
var img = new Image();
img.src = c.bg;
});
}
// After a companion finishes loading, update its skeleton card in the picker if open
function _refreshPickerCard(companionId, bgSrc) {
var btn = document.querySelector('.companion-pick-btn[data-cid="' + companionId + '"]');
if (!btn) return;
btn.classList.remove('cpb-loading');
btn.style.backgroundImage = 'url(' + bgSrc + ')';
}
// Preload every companion sequentially in background; update picker cards live
function _preloadAllInBackground() {
var i = 0;
function next() {
if (i >= _COMPANIONS.length) return;
var c = _COMPANIONS[i++];
if (_companionCache[c.id] && _companionCache[c.id].length > 0) {
_refreshPickerCard(c.id, c.bg); // already done â just make sure card is shown
next();
return;
}
_loadCompanionFrames(c).then(function() {
_refreshPickerCard(c.id, c.bg);
setTimeout(next, 200);
});
}
next(); // start immediately â no extra delay
}
function _startCompanionAnimation() {
if (_companionIntervalId) return;
const spriteEl = document.getElementById('catCompanion');
if (!spriteEl || _companionFrames.length === 0) return;
_companionIntervalId = setInterval(function() {
_companionFrameIdx = (_companionFrameIdx + 1) % _companionFrames.length;
spriteEl.src = _companionFrames[_companionFrameIdx];
const card = document.querySelector('.focus-world-card');
const maxX = card ? card.clientWidth - _COMPANION_HEIGHT_PX - 10 : 440;
_companionPosX += _companionSpeed * _companionDir;
if (_companionPosX >= maxX) {
_companionPosX = maxX;
_companionDir = -1;
spriteEl.style.transform = 'scaleX(-1)';
} else if (_companionPosX <= 0) {
_companionPosX = 0;
_companionDir = 1;
spriteEl.style.transform = 'scaleX(1)';
}
spriteEl.style.left = _companionPosX + 'px';
}, 1000 / _COMPANION_FPS);
}
function _pauseCompanionAnimation() {
if (_companionIntervalId) {
clearInterval(_companionIntervalId);
_companionIntervalId = null;
}
}
async function _switchCompanion(idx) {
_activeCompanionIdx = idx;
_saveCompanionIdx();
const companion = _COMPANIONS[idx];
const bgEl = document.getElementById('focusWorldBg');
const nameEl = document.getElementById('companionName');
const spriteEl = document.getElementById('catCompanion');
const card = document.querySelector('.focus-world-card');
if (bgEl) bgEl.src = companion.bg;
if (nameEl) nameEl.textContent = companion.name;
_pauseCompanionAnimation();
_companionFrames = [];
_companionFrameIdx = 0;
_companionPosX = 0;
_companionDir = 1;
// Hide sprite + shimmer while first batch downloads
if (spriteEl) {
spriteEl.style.transition = 'none';
spriteEl.style.opacity = '0';
spriteEl.style.left = '0px';
spriteEl.style.transform = 'scaleX(1)';
}
if (card) card.classList.add('fw-companion-loading');
// Progressive load: shows companion after just the first QUICK frames,
// then silently pushes the rest into the live array while animating
await _switchCompanionProgressive(companion);
if (_companionFrames.length === 0) {
_companionFrames = [companion.folder + (companion.prefix || '') + String(companion.start).padStart(4, '0') + '.png'];
}
// Frames ready â fade in
if (spriteEl && _companionFrames.length > 0) {
spriteEl.src = _companionFrames[0];
spriteEl.style.display = 'block';
spriteEl.dataset.companion = companion.id;
requestAnimationFrame(function() {
spriteEl.style.transition = 'opacity 0.3s ease';
spriteEl.style.opacity = '1';
setTimeout(function() { spriteEl.style.transition = 'none'; }, 350);
});
}
if (card) card.classList.remove('fw-companion-loading');
// Only walk if timer is already running; otherwise stay idle until Start is pressed
if (typeof isRunning !== 'undefined' && isRunning) {
_startCompanionAnimation();
}
}
// Override setAnimalAnimationPaused so existing timer code drives companion animation
function setAnimalAnimationPaused(isPaused) {
if (isPaused) {
_pauseCompanionAnimation();
} else {
_startCompanionAnimation();
}
const circleHamster = document.getElementById('circleHamster');
if (circleHamster) { circleHamster.classList.toggle('paused', !!isPaused); }
}
// --- Companion Picker ---
function _buildCompanionPicker() {
const picker = document.getElementById('companionPicker');
if (!picker) return;
picker.innerHTML = '';
const grid = document.createElement('div');
grid.className = 'companion-picker-grid';
_COMPANIONS.forEach(function(c, i) {
const cached = _companionCache[c.id] && _companionCache[c.id].length > 0;
const btn = document.createElement('button');
btn.className = 'companion-pick-btn'
+ (i === _activeCompanionIdx ? ' active' : '')
+ (cached ? '' : ' cpb-loading');
btn.dataset.cid = c.id;
btn.title = c.name;
if (cached) {
btn.style.backgroundImage = 'url(' + c.bg + ')';
}
const label = document.createElement('span');
label.className = 'cpb-name';
label.textContent = c.name;
btn.appendChild(label);
if (!cached) {
const spinner = document.createElement('span');
spinner.className = 'cpb-spinner';
btn.appendChild(spinner);
}
btn.addEventListener('click', function() {
_switchCompanion(i);
_closePicker();
});
grid.appendChild(btn);
});
picker.appendChild(grid);
}
function _openPicker() {
const picker = document.getElementById('companionPicker');
if (!picker) return;
const btn = document.getElementById('changeCompanionBtn');
if (btn) {
const rect = btn.getBoundingClientRect();
picker.style.left = Math.max(10, rect.left - 80) + 'px';
picker.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
picker.style.top = 'auto';
}
_buildCompanionPicker();
picker.classList.remove('hidden');
}
function _closePicker() {
const picker = document.getElementById('companionPicker');
if (picker) picker.classList.add('hidden');
}
(function() {
const changeBtn = document.getElementById('changeCompanionBtn');
if (changeBtn) {
function _togglePicker(e) {
e.stopPropagation();
e.preventDefault();
const picker = document.getElementById('companionPicker');
if (picker && picker.classList.contains('hidden')) {
_openPicker();
} else {
_closePicker();
}
}
changeBtn.addEventListener('click', _togglePicker);
changeBtn.addEventListener('touchend', function(e) {
// touchend â synthetic click also fires; handle here and cancel the click
if (e.cancelable) e.preventDefault();
_togglePicker(e);
}, { passive: false });
}
document.addEventListener('click', function(e) {
if (!e.target.closest('#companionPicker') && !e.target.closest('#changeCompanionBtn')) {
_closePicker();
}
});
})();
// --- Focus Level / XP system ---
const _FOCUS_XP_PER_SESSION = 120;
function _getFocusMaxXP(level) {
return Math.round(500 * Math.pow(1.3, level - 1));
}
let _focusLevel = (function() {
const v = parseInt(localStorage.getItem('fliqlow_focus_level'), 10);
return (v && v > 0) ? v : 1;
})();
let _focusXP = (function() {
const v = parseInt(localStorage.getItem('fliqlow_focus_xp'), 10);
return (v && v >= 0) ? v : 0;
})();
function _saveFocusData() {
localStorage.setItem('fliqlow_focus_level', String(_focusLevel));
localStorage.setItem('fliqlow_focus_xp', String(_focusXP));
}
function _renderFocusLevelUI() {
const maxXP = _getFocusMaxXP(_focusLevel);
const elLv = document.getElementById('focusLevel');
const elCur = document.getElementById('currentXP');
const elMax = document.getElementById('maxXP');
const elFill = document.getElementById('xpFill');
if (elLv) elLv.textContent = _focusLevel;
if (elCur) elCur.textContent = _focusXP;
if (elMax) elMax.textContent = maxXP;
if (elFill) {
const pct = Math.min(100, (_focusXP / maxXP) * 100);
elFill.style.height = pct + '%';
}
}
function addFocusXP(amount) {
_focusXP += amount;
let maxXP = _getFocusMaxXP(_focusLevel);
while (_focusXP >= maxXP) {
_focusXP -= maxXP;
_focusLevel++;
maxXP = _getFocusMaxXP(_focusLevel);
}
_saveFocusData();
_renderFocusLevelUI();
}
// Extend finishPomodoroSession to award XP
(function() {
const _orig = window.finishPomodoroSession;
window.finishPomodoroSession = function() {
if (typeof _orig === 'function') _orig.call(this);
addFocusXP(_FOCUS_XP_PER_SESSION);
};
})();
// Init: preload bg images immediately, load active companion, then preload all frames
_preloadBgImages();
_switchCompanion(_activeCompanionIdx).then(_preloadAllInBackground);
// Initial XP render
_renderFocusLevelUI();
// ==========================================
// PINCH-TO-SCALE â focus world companion widget
// ==========================================
(function() {
const wrapper = document.getElementById('focusWorldWrapper');
if (!wrapper) return;
const SCALE_MIN = 0.4;
const SCALE_MAX = 1.2;
const SCALE_KEY = 'fliqlow_fw_scale';
let _scale = parseFloat(localStorage.getItem(SCALE_KEY)) || 1;
_scale = Math.min(SCALE_MAX, Math.max(SCALE_MIN, _scale));
wrapper.style.setProperty('--fw-scale', _scale);
let _startDist = 0;
let _startScale = 1;
function _dist(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
wrapper.addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
_startDist = _dist(e.touches);
_startScale = _scale;
}
}, { passive: true });
wrapper.addEventListener('touchmove', function(e) {
if (e.touches.length === 2) {
e.preventDefault();
const ratio = _dist(e.touches) / _startDist;
_scale = Math.min(SCALE_MAX, Math.max(SCALE_MIN, _startScale * ratio));
wrapper.style.setProperty('--fw-scale', _scale);
}
}, { passive: false });
wrapper.addEventListener('touchend', function(e) {
if (e.touches.length < 2) {
localStorage.setItem(SCALE_KEY, String(_scale));
}
}, { passive: true });
})();
// ==========================================
// PAGE-NAV AUTO-HIDE ON INACTIVITY
// ==========================================
(function() {
const nav = document.querySelector('.page-nav');
if (!nav) return;
const HIDE_DELAY = 2800;
let _hideTimer = null;
function showNav() {
nav.classList.remove('page-nav--hidden');
clearTimeout(_hideTimer);
_hideTimer = setTimeout(function() {
nav.classList.add('page-nav--hidden');
}, HIDE_DELAY);
}
// Any of these events resets the timer and shows the nav
var _events = ['scroll', 'mousemove', 'touchstart', 'keydown'];
_events.forEach(function(evt) {
document.addEventListener(evt, showNav, { passive: true });
});
// Visible on page load, then auto-hides after HIDE_DELAY
showNav();
})();